├── _config.yml ├── .travis.yml ├── .drone.yaml ├── .done.yml ├── config.js ├── db ├── models │ ├── Accounts.js │ ├── Portfolios.js │ ├── Earnings.js │ ├── ExchangeRates.js │ ├── StockHistory.js │ └── PortfolioStocks.js └── sql │ └── sql.js ├── dbConsole.js ├── package.json ├── LICENSE ├── .gitignore ├── README.md ├── index.js └── lib └── service └── stockService.js /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-slate -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "node" 4 | -------------------------------------------------------------------------------- /.drone.yaml: -------------------------------------------------------------------------------- 1 | pipeline: 2 | build: 3 | image: node:latest 4 | commands: 5 | - npm install 6 | -------------------------------------------------------------------------------- /.done.yml: -------------------------------------------------------------------------------- 1 | pipeline: 2 | build: 3 | image: node:latest 4 | commands: 5 | - npm install 6 | 7 | -------------------------------------------------------------------------------- /config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | apiUrl: "https://www.alphavantage.co/query?function=TIME_SERIES_DAILY&outputsize=full&", 3 | cronSchedule: "0 0 20 * * *", 4 | apiKeyPath: "./apiKey" 5 | }; -------------------------------------------------------------------------------- /db/models/Accounts.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const Sequelize = require("sequelize"); 3 | 4 | module.exports = function(sequelize) { 5 | const Accounts = sequelize.define('accounts', { 6 | id: { 7 | type: Sequelize.INTEGER, 8 | autoIncrement: true, 9 | primaryKey: true 10 | }, 11 | name: { 12 | type: Sequelize.STRING, 13 | unique: true 14 | } 15 | }); 16 | return Accounts; 17 | }; 18 | -------------------------------------------------------------------------------- /db/models/Portfolios.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const Accounts = require("./Accounts"); 3 | const Sequelize = require("sequelize"); 4 | 5 | module.exports = function(sequelize) { 6 | const Portfolios = sequelize.define('portfolios', { 7 | id: { 8 | type: Sequelize.INTEGER, 9 | autoIncrement: true, 10 | primaryKey: true 11 | }, 12 | type: { 13 | type: Sequelize.STRING 14 | }, 15 | accountId: { 16 | type: Sequelize.INTEGER 17 | } 18 | }); 19 | return Portfolios; 20 | }; 21 | -------------------------------------------------------------------------------- /db/models/Earnings.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const Sequelize = require("sequelize"); 3 | 4 | module.exports = function(sequelize, DataTypes) { 5 | const Earnings = sequelize.define('earnings', { 6 | id: { 7 | type: Sequelize.INTEGER, 8 | autoIncrement: true, 9 | primaryKey: true 10 | }, 11 | fromStock: { 12 | type: Sequelize.STRING 13 | }, 14 | amount: { 15 | type: Sequelize.DECIMAL 16 | }, 17 | timestamp: { 18 | allowNull: false, 19 | type: Sequelize.DATE, 20 | } 21 | }); 22 | return Earnings; 23 | }; 24 | -------------------------------------------------------------------------------- /db/models/ExchangeRates.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const Sequelize = require("sequelize"); 3 | 4 | module.exports = function(sequelize) { 5 | const ExchangeRates = sequelize.define('exchangeRates', { 6 | id: { 7 | type: Sequelize.INTEGER, 8 | autoIncrement: true, 9 | primaryKey: true 10 | }, 11 | currencyFrom: { 12 | type: Sequelize.STRING 13 | }, 14 | currencyTo: { 15 | type: Sequelize.STRING 16 | }, 17 | ratio: { 18 | type: Sequelize.DECIMAL 19 | }, 20 | timestamp: { 21 | allowNull: false, 22 | type: Sequelize.DATE, 23 | } 24 | }); 25 | return ExchangeRates; 26 | }; 27 | -------------------------------------------------------------------------------- /db/models/StockHistory.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const Sequelize = require("sequelize"); 3 | 4 | module.exports = function(sequelize) { 5 | const StockHistory = sequelize.define('stockHistory', { 6 | id: { 7 | type: Sequelize.INTEGER, 8 | autoIncrement: true, 9 | primaryKey: true 10 | }, 11 | symbol: { 12 | type: Sequelize.STRING 13 | }, 14 | open: { 15 | type: Sequelize.DECIMAL 16 | }, 17 | close: { 18 | type: Sequelize.DECIMAL 19 | }, 20 | volume: { 21 | type: Sequelize.INTEGER 22 | }, 23 | timestamp: { 24 | allowNull: false, 25 | type: Sequelize.INTEGER 26 | } 27 | }); 28 | return StockHistory; 29 | }; 30 | -------------------------------------------------------------------------------- /db/models/PortfolioStocks.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const Portfolios = require("./Portfolios"); 3 | const Sequelize = require("sequelize"); 4 | 5 | module.exports = function(sequelize) { 6 | const PortfolioStocks = sequelize.define('portfolioStocks', { 7 | id: { 8 | type: Sequelize.INTEGER, 9 | autoIncrement: true, 10 | primaryKey: true 11 | }, 12 | symbol: { 13 | type: Sequelize.STRING 14 | }, 15 | purchaseTime: { 16 | type: Sequelize.INTEGER 17 | }, 18 | quantity: { 19 | type: Sequelize.INTEGER 20 | }, 21 | purchasePrice: { 22 | type: Sequelize.FLOAT 23 | }, 24 | portfolioId: { 25 | type: Sequelize.INTEGER 26 | } 27 | }); 28 | return PortfolioStocks; 29 | }; 30 | -------------------------------------------------------------------------------- /dbConsole.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const sqlite3 = require("sqlite3").verbose(); 4 | const config = require("./config"); 5 | 6 | 7 | 8 | const db = new sqlite3.Database('./db/stocks.db', sqlite3.OPEN_READWRITE | sqlite3.OPEN_CREATE, (err) => { 9 | if (err) { 10 | console.error(err.message); 11 | } 12 | console.log('Connected to the stats database.'); 13 | }); 14 | 15 | db.on('trace', function(msg) { 16 | console.log(msg); 17 | }); 18 | var stdin = process.openStdin(); 19 | let fullInput = ""; 20 | stdin.addListener("data", function(input) { 21 | fullInput += input.toString() + " "; 22 | if(input.toString().indexOf(";") !== -1) { 23 | db.all(fullInput, function(err, res) { 24 | console.log(err); 25 | console.log("results:" + JSON.stringify(res)); 26 | }); 27 | fullInput = ""; 28 | } 29 | }); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stock-tracker", 3 | "version": "0.0.1", 4 | "description": "Read stock data from API and save to db", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"TODO: Add tests\" && exit 0" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/alecc08/stock-tracker.git" 12 | }, 13 | "keywords": [ 14 | "stocks", 15 | "data" 16 | ], 17 | "author": "alecc08", 18 | "license": "MIT", 19 | "bugs": { 20 | "url": "https://github.com/alecc08/stock-tracker/issues" 21 | }, 22 | "homepage": "https://github.com/alecc08/stock-tracker#readme", 23 | "dependencies": { 24 | "bluebird": "^3.5.1", 25 | "express": "^4.16.2", 26 | "lodash": "^4.17.4", 27 | "moment": "^2.19.1", 28 | "node-cron": "^1.2.1", 29 | "request": "^2.83.0", 30 | "sequelize": "^5.3.0", 31 | "sqlite3": "^3.1.13" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Alec Chamberland 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # don't commit db file 61 | stocks.db 62 | 63 | # don't commit private API key 64 | apiKey -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # stock-tracker [![Build Status](https://travis-ci.org/alecc08/stock-tracker.svg?branch=master)](https://travis-ci.org/alecc08/stock-tracker) [![Codacy Badge](https://api.codacy.com/project/badge/Grade/f858fc237304469f812601459d3f3e29)](https://www.codacy.com/app/alecc/stock-tracker?utm_source=github.com&utm_medium=referral&utm_content=alecc08/stock-tracker&utm_campaign=Badge_Grade) 2 | NodeJS stock tracker. Read historical stock data from an API to store in local DB. Offer API to integrate data into a dashboard. https://github.com/alecc08/stock-tracker-dashboard 3 | 4 | Using Alphavantage API to get data. You can get your own API key from them at https://www.alphavantage.co 5 | 6 | ### Note: Alphavantage is free but they request not to query them over 100 calls per minute. Please respect this limit or contact them to work something out. This application shouldn't have any need get anywhere near that volume, as the data returned will be from the database unless the stock wasn't already obtained. 7 | 8 | ## API Endpoints: 9 | 10 | ### Update a stock: 11 | ``` 12 | POST 13 | /stocks 14 | 15 | - Params 16 | -stockCode 17 | 18 | Ex: POST /stocks stockCode:MSFT 19 | ``` 20 | 21 | ### Auto update stocks 22 | There's a cron job that runs at your time of choosing to update all stocks in the database. You can change this time in ```config.js``` 23 | 24 | ### Get a stock: 25 | ``` 26 | GET 27 | /stocks 28 | 29 | - Params 30 | -stockCodes : Comma seperated list of stocks 31 | -start : YYYY-MM-DD format date start 32 | -end : YYYY-MM-DD format date end 33 | 34 | Ex: GET /stocks stockCodes:MSFT,FB,GOOG start:2017-01-01 end:2017-02-02 35 | ``` 36 | 37 | ### Get an account: 38 | ``` 39 | GET 40 | /accounts 41 | 42 | - Params 43 | -accountId 44 | 45 | Ex: POST /stocks stockCode:MSFT 46 | ``` 47 | 48 | ### Create an account: 49 | ``` 50 | POST 51 | /accounts 52 | - Params 53 | -accountName 54 | ``` 55 | 56 | ### Run sql on DB 57 | 58 | > To run some custom sql directly into the db, simply run the dbConsole.js file with node 59 | ``` 60 | node dbConsole 61 | ``` 62 | >Then you can run any sql statement followed by a semicolon to execute the query. E.g. 63 | ``` 64 | SELECT * FROM stock_history; 65 | ``` -------------------------------------------------------------------------------- /db/sql/sql.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | module.exports = { 3 | createStockTable: `CREATE TABLE IF NOT EXISTS 4 | stock_history ( 5 | 6 | stock TEXT, 7 | timestamp INTEGER, 8 | open NUMBER, 9 | close NUMBER, 10 | volume INTEGER 11 | 12 | ) 13 | `, 14 | 15 | createAccountTable: ` CREATE TABLE IF NOT EXISTS 16 | accounts( 17 | 18 | id INTEGER PRIMARY KEY, 19 | name TEXT 20 | 21 | ) 22 | `, 23 | 24 | createPortfolioTable: `CREATE TABLE IF NOT EXISTS 25 | portfolios( 26 | 27 | id INTEGER PRIMARY KEY AUTOINCREMENT, 28 | account_id INTEGER, 29 | type TEXT, 30 | 31 | FOREIGN KEY(account_id) REFERENCES account(id) 32 | ) 33 | `, 34 | 35 | createPortfolioStockTable: `CREATE TABLE IF NOT EXISTS 36 | portfolio_stocks( 37 | id INTEGER PRIMARY KEY AUTOINCREMENT, 38 | portfolio_id INTEGER, 39 | stock TEXT, 40 | purchase_timestamp INTEGER, 41 | purchase_qty INTEGER, 42 | purchase_price NUMBER, 43 | 44 | FOREIGN KEY(portfolio_id) REFERENCES portfolio(id) 45 | ) 46 | `, 47 | 48 | createStockSplitTable: `CREATE TABLE IF NOT EXISTS 49 | stock_splits( 50 | stock TEXT, 51 | factor NUMBER, 52 | timestamp INTEGER 53 | ) 54 | `, 55 | 56 | createExchangeRateTable: `CREATE TABLE IF NOT EXISTS 57 | exchange_rates( 58 | currencyFrom TEXT, 59 | currencyTo TEXT, 60 | ratio NUMBER, 61 | timestamp INTEGER 62 | ) 63 | `, 64 | 65 | createEarningsTable: `CREATE TABLE IF NOT EXISTS 66 | earnings( 67 | amount NUMBER, 68 | timestamp INTEGER, 69 | from_stock TEXT, 70 | from_portfolio INTEGER, 71 | from_account INTEGER 72 | ) 73 | `, 74 | 75 | 76 | findAllStocksIn: "SELECT * FROM stock_history WHERE stock IN ", 77 | stocksWithinRange: " AND timestamp >= ? AND timestamp <= ? ORDER BY timestamp ASC", 78 | findStockBySymbol: "SELECT * FROM stock_history WHERE stock=?", 79 | insertStock: "INSERT INTO stock_history(stock,timestamp,open,close,volume) VALUES ", 80 | getStockDatesByCode: "SELECT timestamp FROM stock_history WHERE stock=?", 81 | getAllStockCodes: "SELECT DISTINCT stock FROM stock_history", 82 | 83 | 84 | insertAccount: "INSERT INTO accounts(name) VALUES (?)", 85 | getAccounts: "SELECT * from accounts", 86 | getAccountsWithPortfolios: "SELECT a.id, a.name, p.type, p.id as portfolioId FROM accounts a LEFT JOIN portfolios p ON a.id = p.account_id;", 87 | deleteAccountById: "DELETE FROM accounts WHERE id=?", 88 | 89 | getPortfolioWithStocks: "SELECT p.type, p.id, s.stock, s.purchase_timestamp, s.purchase_qty, s.purchase_price, s.id as stock_id FROM portfolios p LEFT JOIN portfolio_stocks s ON s.portfolio_id = p.id WHERE p.id = ?", 90 | insertPortfolio: "INSERT INTO portfolios(type, account_id) VALUES(?,?)", 91 | deletePortfolioById: "DELETE FROM portfolios WHERE id=?", 92 | 93 | insertPortfolioStock: "INSERT INTO portfolio_stocks(portfolio_id, stock, purchase_qty, purchase_price, purchase_timestamp) VALUES(?,?,?,?,?)", 94 | updatePortfolioStock: "UPDATE portfolio_stocks SET purchase_qty = ?, purchase_price = ?, purchase_timestamp = ? WHERE id = ?", 95 | deletePortfolioStock: "DELETE FROM portfolio_stocks WHERE id = ?" 96 | }; -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const request = require("request"); 4 | const sqlite3 = require("sqlite3").verbose(); 5 | const config = require("./config"); 6 | const stockService = require("./lib/service/stockService"); 7 | const cron = require("node-cron"); 8 | 9 | const express = require("express"); 10 | var bodyParser = require('body-parser'); 11 | 12 | stockService.initDB(); 13 | 14 | let app = express(); 15 | 16 | // Schedule the stock updates 17 | cron.schedule(config.cronSchedule, function() { 18 | // Update all existing stocks 19 | 20 | stockService.getAllStocksInDb(function(stocks) { 21 | if(stocks && stocks.length > 0) { 22 | let counter = 0; 23 | const WAIT_BETWEEN_CALLS = 1000; // 1 second 24 | stocks.forEach(function(stock) { 25 | // This part may get too intensive if tracking lots of stocks. 26 | // Please be respectful of Alphavantage's suggestion of max requests per minute 27 | setTimeout(function() { 28 | console.log("Updating " + stock.symbol); 29 | stockService.updateStock(stock.symbol); 30 | }, counter * WAIT_BETWEEN_CALLS); 31 | counter++; 32 | }); 33 | } else { 34 | console.log("No stocks to update"); 35 | } 36 | }); 37 | }); 38 | 39 | app.use(function(req, res, next) { 40 | res.header("Access-Control-Allow-Origin", "*"); 41 | res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept"); 42 | res.header("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,OPTIONS"); 43 | next(); 44 | }); 45 | 46 | app.use(bodyParser.urlencoded({ extended: true })); //Use to process POST params 47 | app.use(bodyParser.json()); //Use to process POST params 48 | 49 | app.get("/stocks", function(req, res) { 50 | if(req.query.stockCodes) { 51 | let requestedStocks = req.query.stockCodes.split(",").map((stock) => stock.trim()); 52 | let startDate = req.query.start; 53 | let endDate = req.query.end; 54 | 55 | let stockData = []; 56 | stockService.findAllForRange(requestedStocks, startDate, endDate, function(data) { 57 | res.status(200).send(data); 58 | }); 59 | 60 | } else { 61 | res.status(400).send("Please specify stockCodes separated by ','. Ex: stockCodes=MSFT,GOOG,AAPL"); 62 | } 63 | 64 | }); 65 | 66 | app.post("/stocks", function(req, res) { 67 | if(req.body.stockCode) { 68 | console.log("Updating " + req.body.stockCode); 69 | stockService.updateStock(req.body.stockCode); 70 | res.status(200).send(); 71 | } else { 72 | res.status(400).send("Please specify stockCode"); 73 | } 74 | 75 | }); 76 | 77 | app.get("/accounts", function(req, res) { 78 | stockService.getAccountsWithPortfolios(function(accounts) { 79 | //Also get their portfolios 80 | res.status(200).send(accounts); 81 | }); 82 | }); 83 | 84 | app.post("/accounts", function(req, res) { 85 | console.log(req.body); 86 | if(req.body.accountName) { 87 | stockService.addAccount(req.body.accountName, function(account) { 88 | res.status(200).send({success:true}); 89 | }); 90 | 91 | } else { 92 | res.status(400).send({error:"No name specified"}); 93 | } 94 | 95 | }); 96 | 97 | app.delete("/accounts", function(req, res) { 98 | if(req.query.accountId) { 99 | console.log("Deleting account: " + req.query.accountId); 100 | stockService.deleteAccount(req.query.accountId); 101 | res.status(200).send({}); 102 | } else { 103 | res.status(400).send("Please specify an accountName"); 104 | } 105 | }); 106 | 107 | app.get("/portfolios", function(req, res) { 108 | if(req.query.portfolioId) { 109 | 110 | stockService.getPortfolio(req.query.portfolioId, function(portfolio) { 111 | res.status(200).send(portfolio); 112 | }); 113 | } else { 114 | res.status(400).send({error:"No portfolioId specified"}); 115 | } 116 | }); 117 | 118 | app.post("/portfolios", function(req, res) { 119 | console.log(req.body); 120 | if(req.body.accountId && req.body.portfolioName) { 121 | stockService.addPortfolio(req.body.portfolioName, req.body.accountId, function(account) { 122 | res.status(200).send({success:true}); 123 | }); 124 | 125 | } else { 126 | res.status(400).send({error:"No name or accountId specified"}); 127 | } 128 | 129 | }); 130 | 131 | app.put("/portfolios", function(req, res) { 132 | console.log(req.body); 133 | if(req.body.portfolio) { 134 | stockService.updatePortfolio(req.body.portfolio, function(account) { 135 | res.status(200).send({success:true}); 136 | }); 137 | } else { 138 | res.status(400).send({error:"No portfolio to update"}); 139 | } 140 | }); 141 | 142 | app.delete("/portfolios", function(req, res) { 143 | 144 | if(req.query.portfolioId) { 145 | console.log("Deleting portfolio: " + req.query.portfolioId); 146 | stockService.deletePortfolio(req.query.portfolioId); 147 | res.status(200).send({}); 148 | } else { 149 | res.status(400).send("Please specify a portfolioId"); 150 | } 151 | }); 152 | 153 | 154 | 155 | var server = app.listen(process.env.PORT || 8080, function() { 156 | var port = server.address().port; 157 | 158 | console.log("Stock tracker listening at http://localhost:%s", port); 159 | }); -------------------------------------------------------------------------------- /lib/service/stockService.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const sql = require("../../db/sql/sql.js"); 3 | const Moment = require("moment"); 4 | const request = require("request"); 5 | const config = require("../../config"); 6 | const fs = require("fs"); 7 | const apiKey = fs.readFileSync(config.apiKeyPath).toString(); 8 | const Sequelize = require('sequelize'); 9 | const _ = require('lodash'); 10 | 11 | const Op = Sequelize.Op; 12 | const sequelize = new Sequelize('database', '', '', { 13 | dialect: 'sqlite', 14 | operatorsAliases: Op, 15 | pool: { 16 | max: 5, 17 | min: 0, 18 | acquire: 30000, 19 | idle: 10000 20 | }, 21 | storage: 'db/stocks.db' 22 | }); 23 | 24 | console.log("Using API Key: " + apiKey); 25 | 26 | const db = {}; 27 | 28 | module.exports = { 29 | 30 | initDB() { 31 | let files = fs.readdirSync('./db/models'); 32 | files.forEach((file) => { 33 | db[file.split('.')[0]] = require('../../db/models/'+ file)(sequelize); 34 | db[file.split('.')[0]].sync(); 35 | }); 36 | 37 | db.Accounts.hasMany(db.Portfolios); 38 | db.Portfolios.belongsTo(db.Accounts); 39 | db.Portfolios.hasMany(db.PortfolioStocks, {as:'stocks'}); 40 | db.PortfolioStocks.belongsTo(db.Portfolios); 41 | console.log(JSON.stringify(files)); 42 | }, 43 | 44 | 45 | /* ========================== STOCK SECTION =========================== */ 46 | findAllForRange(stocks, start, end, cb) { 47 | let startDate = new Moment(start, "YYYY-MM-DD"); 48 | let endDate = new Moment(end, "YYYY-MM-DD"); 49 | db.StockHistory.findAll({where: {symbol: {[Op.in]:stocks}, timestamp: { 50 | [Op.lt]: Moment(end, 'YYYY-MM-DD').unix(), 51 | [Op.gt]: Moment(start, 'YYYY-MM-DD').unix() 52 | }}, order: [['timestamp','ASC']] 53 | }).then(results => { 54 | let grouped = {}; 55 | results.forEach(function(row) { 56 | if(!grouped[row.symbol]) { 57 | grouped[row.symbol] = []; 58 | } 59 | grouped[row.symbol].push(row); 60 | }); 61 | cb(grouped); 62 | }); 63 | 64 | }, 65 | updateStock(stockCode) { 66 | let path = config.apiUrl + "symbol=" + stockCode + "&apikey=" + apiKey; 67 | console.log("Calling: " + path); 68 | request.get(path, function(err, res) { 69 | try { 70 | res.body = JSON.parse(res.body); 71 | } catch(e) { 72 | console.log("\n\n\n\nERROR!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n Failed to update stocks\n\n\n\n\n\n"); 73 | return; 74 | } 75 | 76 | if(!res.body["Meta Data"]) { 77 | console.log("Failed to find " + stockCode); 78 | return; 79 | } 80 | console.log(res.body["Meta Data"]); 81 | let allInserts = []; 82 | db.StockHistory.findAll({ where: { symbol: stockCode }, raw: true }).then((stockHistory) => { 83 | let existingTimestamps = []; 84 | stockHistory.forEach(stockHistory => { 85 | existingTimestamps.push(stockHistory.timestamp); 86 | }); 87 | Object.keys(res.body["Time Series (Daily)"]).forEach(function(key) { 88 | let timestampUnix = Moment(key, 'YYYY-MM-DD').unix(); 89 | if(existingTimestamps.indexOf(timestampUnix) === -1) { 90 | let stockDate = res.body["Time Series (Daily)"][key]; 91 | db.StockHistory.create({symbol: stockCode, timestamp: timestampUnix, open: stockDate["1. open"], close: stockDate["4. close"], volume: stockDate["5. volume"]}); 92 | } 93 | 94 | }); 95 | }); 96 | }); 97 | }, 98 | getAllStocksInDb(callback) { 99 | db.StockHistory.findAll({}).then((stockHistories) => { 100 | callback(stockHistories); 101 | }); 102 | }, 103 | 104 | 105 | 106 | 107 | 108 | /* ========================== ACCOUNT SECTION =========================== */ 109 | getAccounts(callback) { 110 | db.Accounts.findAll({}).then(callback); 111 | 112 | }, 113 | getAccountsWithPortfolios(callback) { 114 | db.Accounts.findAll({include: [{ 115 | model: db.Portfolios, include: {model:db.PortfolioStocks, as: 'stocks'} 116 | }]}).then(callback); 117 | }, 118 | addAccount(accountName, callback) { 119 | db.Accounts.create({name: accountName}).then(callback); 120 | }, 121 | deleteAccount(accountId, callback) { 122 | //db.Accounts(sql.deleteAccountById, [accountId], callback); 123 | }, 124 | 125 | 126 | 127 | 128 | /* ========================== PORTFOLIO SECTION =========================== */ 129 | getPortfolio(id, callback) { 130 | db.Portfolios.findAll({ 131 | where: {id:id}, 132 | include: { 133 | model: db.PortfolioStocks, 134 | as: 'stocks' 135 | } 136 | }).then((portfolios) => { 137 | callback(portfolios[0]); 138 | }); 139 | }, 140 | addPortfolio(type, accountId, callback) { 141 | db.Portfolios.create({type: type, accountId: accountId}).then(callback); 142 | }, 143 | 144 | updatePortfolio(portfolio, callback) { 145 | //Update portfolio is just updating its stocks, not the portfolio table itself 146 | console.log("Updating portfolio:" + JSON.stringify(portfolio)); 147 | portfolio.stocks.forEach((stock) => { 148 | if(!stock.id) { 149 | this.updateStock(stock.symbol); 150 | db.PortfolioStocks.create({symbol:stock.symbol, purchaseTime: stock.purchaseTime, quantity: stock.purchaseQuantity, purchasePrice: stock.purchasePrice, portfolioId: portfolio.id}).then(()=> { 151 | console.log("Saved new stock"); 152 | }); 153 | } 154 | }); 155 | 156 | callback(null, "Success"); 157 | }, 158 | 159 | deletePortfolio(db, portfolioId, callback) { 160 | db.all(sql.deletePortfolioById, [portfolioId], callback); 161 | } 162 | }; --------------------------------------------------------------------------------