algotrader
Version:
Algorithmically trade stocks and options using Robinhood, Yahoo Finance, and more.
650 lines (603 loc) • 17.8 kB
JavaScript
const Robinhood = require('./Robinhood');
const Fundamentals = require('./Fundamentals');
const Market = require('./Market');
const Quote = require('../../globals/Quote');
const LibraryError = require('../../globals/LibraryError');
const request = require('request');
const async = require('async');
/**
* Represents a security traded on Robinhood.
*/
class Instrument extends Robinhood {
/**
* Creates a new Instrument object.
* @author Torrey Leonard <https://github.com/Ladinn>
* @constructor
* @param {Object} object
*/
constructor(object) {
if (!object instanceof Object) throw new Error("Parameter 'object' must be an object.");
else {
super();
this.name = String(object.name);
this.simpleName = String(object.simple_name);
this.symbol = String(object.symbol);
this.listDate = new Date(object.list_date);
this.country = String(object.country);
this.tradeable = Boolean(object.tradeable);
this.type = String(object.type);
this.bloomberg = String(object.bloomberg_unique);
this.state = String(object.state);
this.id = String(object.id);
this.urls = {
market: String(object.market),
fundamentals: String(object.fundamentals),
quote: String(object.quote),
instrument: String(object.url),
splits: String(object.splits)
};
this.margin = {
initialRatio: Number(object.margin_initial_ratio),
dayTradeRatio: Number(object.day_trade_ratio),
maintenanceRatio: Number(object.maintenance_ratio)
}
}
}
// Static initializer methods
/**
* Returns an array of all available instruments.
* WARNING: this will take a while!
* @author Torrey Leonard <https://github.com/Ladinn>
* @returns {Promise<Array>}
*/
static getAll() {
return new Promise((resolve, reject) => {
request({
uri: "https://api.robinhood.com/instruments/"
}, (error, response, body) => {
return Robinhood.handleResponse(error, response, body, null, res => {
let array = [];
res.forEach(obj => {
array.push(new Instrument(obj));
});
resolve(array);
}, reject);
})
})
}
/**
* Returns an instrument object for the specified symbol.
* @author Torrey Leonard <https://github.com/Ladinn>
* @param {String} symbol
* @returns {Promise<Instrument>}
*/
static getBySymbol(symbol) {
return new Promise((resolve, reject) => {
if (!symbol instanceof String) reject(new Error("Parameter 'symbol' must be a string."));
else request({
uri: "https://api.robinhood.com/instruments/",
qs: {
symbol: symbol
}
}, (error, response, body) => {
return Robinhood.handleResponse(error, response, body, null, res => {
if (res.length !== undefined) reject(new LibraryError("Invalid instrument symbol."));
else resolve(new Instrument(res));
}, reject);
})
})
}
/**
* Returns an instrument object for the specified Robinhood instrument ID.
* @author Torrey Leonard <https://github.com/Ladinn>
* @param {String} id
* @returns {Promise<Instrument>}
*/
static getByID(id) {
return new Promise((resolve, reject) => {
if (!id instanceof String) reject(new Error("Parameter 'id' must be a string."));
else request({
uri: "https://api.robinhood.com/instruments/" + id + "/"
}, (error, response, body) => {
return Robinhood.handleResponse(error, response, body, null, res => {
resolve(new Instrument(res));
}, reject);
})
})
}
/**
* Returns an instrument object for the specified instrument URL.
* @author Torrey Leonard <https://github.com/Ladinn>
* @param {String} instrumentURL
* @returns {Promise<Instrument>}
*/
static getByURL(instrumentURL) {
return new Promise((resolve, reject) => {
if (!instrumentURL instanceof String) reject(new Error("Parameter 'instrumentURL' must be a string."));
request({
uri: instrumentURL
}, (error, response, body) => {
return Robinhood.handleResponse(error, response, body, null, res => {
resolve(new Instrument(res));
}, reject);
})
})
}
/**
* Returns an array of Instruments for 10 of the top moving S&P 500 equities.
* @author Torrey Leonard <https://github.com/Ladinn>
* @param {String} direction - Possible options: [up, down]
* @returns {Promise<Instrument>}
*/
static getTopMoving(direction) {
return new Promise((resolve, reject) => {
request({
uri: "https://api.robinhood.com/midlands/movers/sp500/",
qs: {
direction: direction.toLowerCase()
}
}, (error, response, body) => {
Robinhood.handleResponse(error, response, body, null, res => {
let array = [];
async.forEachOf(res, (value, key, callback) => {
Instrument.getByURL(value.instrument_url).then(ins => {
array.push(ins);
callback();
}).catch(error => reject(new RequestError(error)));
}, () => {
resolve(array);
})
}, reject);
});
})
}
/**
* Returns an array of instrument objects for the specified array of IDs.
*
* Note: large arrays will take longer to process and are capped at 50 per request, so multiple
* requests will be sent as the function iterates through the array.
*
* @author Torrey Leonard <https://github.com/Ladinn>
*
* @param {Array} ids
* @returns {Promise<Array>}
*/
static getByIdArray(ids) {
return new Promise((resolve, reject) => {
if (!ids instanceof Array) reject(new Error("Parameter 'ids' must be an array."));
else {
let array = [];
let maxAtOnce = 50;
async.whilst(() => { return ids.length !== 0 }, callback => {
let newIds = ids.slice(0, maxAtOnce);
ids = ids.length >= maxAtOnce ? ids.slice(maxAtOnce, ids.length) : [];
request({
uri: "https://api.robinhood.com/instruments/",
qs: {
ids: newIds.join(',')
}
}, (error, response, body) => {
return Robinhood.handleResponse(error, response, body, null, res => {
res.forEach(o => {
if (o !== null) array.push(new Instrument(o));
});
callback();
}, reject);
})
}, () => {
resolve(array);
});
}
})
}
/**
* Returns an array of known categories that can be used with getByCategory(). This list is non-exhaustive.
* @author Torrey Leonard <https://github.com/Ladinn>
* @returns {Array<String>}
*/
static getCategories() {
return [
"technology", "finance", "etf", "mutual-fund", "entertainment", "female-ceo", "fossil-fuel", "oil-and-gas", "100-most-popular", "large-cap", "investment-trust-or-fund", "consumer-product",
"social-media", "internet", "aerospace", "software-service", "automotive", "upcoming-earnings", "2015-ipo", "2016-ipo", "2017-ipo", "health", "real-estate", "top-movers",
"air-transportation", "semiconductor", "building-material", "material", "retail", "credit-card", "business-service", "advertising-and-marketing", "payment", "video-game", "electronics",
"manufacturing", "conglomerate", "energy", "electric-utilities", "north-america", "us", "engineering", "rental-and-lease", "restaurant", "hospitality", "telecommunications",
"wireless", "internet", "pharmaceutical", "medical", "healthcare", "cap-weighted", "mid-cap", "small-cap", "packaging"
]
}
/**
* Returns an array of Instruments related to the given category.
* @author Torrey Leonard <https://github.com/Ladinn>
* @param {String} category - For possible options see getCategories().
* @returns {Promise<Array>}
*/
static getByCategory(category) {
return new Promise((resolve, reject) => {
request({
uri: "https://api.robinhood.com/midlands/tags/tag/" + category + "/"
}, (error, response, body) => {
Robinhood.handleResponse(error, response, body, null, res => {
let ids = [];
res.instruments.forEach(o => {
ids.push(o.split('instruments/')[1].split('/')[0]);
});
return Instrument.getByIdArray(ids).then(res => resolve(res)).catch(error => reject(error));
}, reject);
});
})
}
/**
* Returns an array of Instruments for the top 100 most popular equities on Robinhood.
* @author Torrey Leonard <https://github.com/Ladinn>
* @returns {Promise<Array>}
*/
static getMostPopular() {
return Instrument.getByCategory("100-most-popular");
}
/**
* Returns an array of Instruments that have upcoming earnings.
* @author Torrey Leonard <https://github.com/Ladinn>
* @returns {Promise.<Array>}
*/
static getUpcomingEarnings() {
return Instrument.getByCategory("upcoming-earnings");
}
/**
* Returns an array of instruments for stocks from Robinhood's recommendations for the given user.
* @author Torrey Leonard <https://github.com/Ladinn>
* @param {User} user - Authenticated user object
* @returns {Promise.<Array>}
*/
static getRecommendations(user) {
return new Promise((resolve, reject) => {
request({
uri: "https://analytics.robinhood.com/instruments/tag/for-you/",
headers: {
'Authorization': 'Bearer ' + user.getAuthToken()
}
}, (error, response, body) => {
Robinhood.handleResponse(error, response, body, null, res => {
let array = [];
console.log(res);
async.forEachOf(res.instruments, (value, key, callback) => {
Instrument.getByID(value.id).then(ins => {
array.push({
reason: value.reason,
InstrumentObject: ins
});
callback();
})
}, error => {
if (error) reject(error);
else resolve(array);
})
}, reject);
});
})
}
// GET from API
/**
* Fills the instrument object with market, fundamental, quote, and split data. Returns an array of Market, Fundamentals, Quote, and Splits objects.
* @author Torrey Leonard <https://github.com/Ladinn>
* @param {User} user - Authenticated user object
* @returns {Promise<Array>}
*/
populate(user) {
const _this = this;
return new Promise((resolve, reject) => {
Promise.all([
_this.getMarket(),
_this.getFundamentals(),
_this.getQuote(user),
_this.getSplits()
]).then(q => {
_this.MarketObject = q[0];
_this.FundamentalsObject = q[1];
_this.QuoteObject = q[2];
_this.splits = q[3];
resolve(q);
}).catch(error => reject(error));
});
}
/**
* Returns an object with information on the market that this instrument trades on.
* @author Torrey Leonard <https://github.com/Ladinn>
* @returns {Promise<Market>}
*/
getMarket() {
return Market.getByURL(this.urls.market);
}
/**
* Returns a new Fundamentals object with information such as open, high, low, close, volume, market cap, and more, on this instrument.
*
* @author Torrey Leonard <https://github.com/Ladinn>
*
* @returns {Promise<Fundamentals>}
*/
getFundamentals() {
return Fundamentals.getByURL(this.urls.fundamentals);
}
/**
* Returns an object with a real-time quote on this instrument.
*
* @author Torrey Leonard <https://github.com/Ladinn>
* @author Colin Gillingham <https://github.com/Gillinghammer> (Added user authentication after Robinhood API update - issue #11)
*
* @param {User} user - Authenticated user object
* @returns {Promise<Quote>}
*/
getQuote(user) {
const _this = this;
return new Promise((resolve, reject) => {
request({
uri: _this.urls.quote,
headers: {
Authorization: 'Bearer ' + user.getAuthToken()
}
}, (error, response, body) => {
return Robinhood.handleResponse(error, response, body, null, res => {
resolve(new Quote(
{
symbol: res.symbol,
date: new Date(res.updated_at),
source: "Robinhood/" + res.last_trade_price_source,
price: {
last: Number(res.last_trade_price) || Number(res.last_extended_hours_trade_price)
},
dom: {
bid: {
price: Number(res.bid_price),
size: Number(res.bid_size)
},
ask: {
price: Number(res.ask_price),
size: Number(res.ask_size)
}
},
meta: {
isHalted: Boolean(res.trading_halted),
hasTraded: Boolean(res.has_traded)
},
original: JSON.stringify(res)
}
));
}, reject);
})
})
}
/**
* Returns an object containing details on past stock splits.
* @author Torrey Leonard <https://github.com/Ladinn>
* @returns {Promise<Object>}
*/
getSplits() {
const _this = this;
return new Promise((resolve, reject) => {
request({
uri: _this.urls.splits
}, (error, response, body) =>
Robinhood.handleResponse(error, response, body, null, resolve, reject)
);
})
}
/**
* Returns an object containing this company's past and future earnings data.
* @author Torrey Leonard <https://github.com/Ladinn>
* @returns {Promise<Object>}
*/
getEarnings() {
const _this = this;
return new Promise((resolve, reject) => {
request({
uri: _this.url + "/marketdata/earnings/",
qs: {
symbol: _this.symbol
}
}, (error, response, body) => {
Robinhood.handleResponse(error, response, body, null, resolve, reject);
})
})
}
/**
* Returns the high, low, and average prices paid for the instrument by other Robinhood users.
* @author Torrey Leonard <https://github.com/Ladinn>
* @author rclai (Discovered API endpoint)
* @returns {Promise<Object>}
*/
getPricesPaid() {
const _this = this;
return new Promise((resolve, reject) => {
request({
uri: "https://analytics.robinhood.com/instruments/price_distribution/" + _this.id
}, (error, response, body) => {
Robinhood.handleResponse(error, response, body, null, res => {
resolve({
high: res.high,
low: res.low,
average: res.average_price,
bins: res.bins
})
}, reject);
})
})
}
/**
* Returns the total amount of open positions on this instrument among all Robinhood users.
*
* @author Torrey Leonard <https://github.com/Ladinn>
* @author rclai (Discovered API endpoint)
*
* @returns {Promise<Number>}
*/
getPopularity() {
const _this = this;
return new Promise((resolve, reject) => {
request({
uri: _this.url + "/instruments/popularity/",
qs: {
ids: _this.id
}
}, (error, response, body) => {
Robinhood.handleResponse(error, response, body, null, res => {
resolve(Number(res.num_open_positions));
}, reject);
})
})
}
/**
* Returns an object containing buy hold, and sell ratings from major financial institutions, along with text describing the rating.
*
* @author Torrey Leonard <https://github.com/Ladinn>
* @author rclai (Discovered API endpoint)
*
* @returns {Promise<Object>}
*/
getRatings() {
const _this = this;
return new Promise((resolve, reject) => {
request({
uri: _this.url + "/midlands/ratings/",
qs: {
ids: _this.id
}
}, (error, response, body) => {
Robinhood.handleResponse(error, response, body, null, resolve, reject);
})
})
}
// GET from object
/**
* @author Torrey Leonard <https://github.com/Ladinn>
* @returns {String}
*/
getName() {
return this.name;
}
/**
* @author Torrey Leonard <https://github.com/Ladinn>
* @returns {String}
*/
getSimpleName() {
return this.simpleName;
}
/**
* @author Torrey Leonard <https://github.com/Ladinn>
* @returns {String}
*/
getSymbol() {
return this.symbol;
}
/**
* @author Torrey Leonard <https://github.com/Ladinn>
* @returns {Date}
*/
getListDate() {
return this.listDate;
}
/**
* @author Torrey Leonard <https://github.com/Ladinn>
* @returns {String}
*/
getCountry() {
return this.country;
}
/**
* @author Torrey Leonard <https://github.com/Ladinn>
* @returns {String}
*/
getType() {
return this.type;
}
/**
* @author Torrey Leonard <https://github.com/Ladinn>
* @returns {String}
*/
getBloombergID() {
return this.bloomberg;
}
/**
* @author Torrey Leonard <https://github.com/Ladinn>
* @returns {String}
*/
getState() {
return this.state;
}
/**
* @author Torrey Leonard <https://github.com/Ladinn>
* @returns {String}
*/
getID() {
return this.id;
}
/**
* @author Torrey Leonard <https://github.com/Ladinn>
* @returns {Number}
*/
getMarginInitialRatio() {
return this.margin.initialRatio;
}
/**
* @author Torrey Leonard <https://github.com/Ladinn>
* @returns {Number}
*/
getDayTradeRatio() {
return this.margin.dayTradeRatio;
}
/**
* @author Torrey Leonard <https://github.com/Ladinn>
* @returns {Number}
*/
getMaintenanceRatio() {
return this.margin.maintenanceRatio;
}
// BOOLEANS
/**
* Checks if the instrument is able to be traded.
* @author Torrey Leonard <https://github.com/Ladinn>
* @returns {Boolean}
*/
isTradeable() {
return this.tradeable;
}
/**
* @author Torrey Leonard <https://github.com/Ladinn>
* @returns {boolean}
*/
isActive() {
return this.state === "active";
}
/**
* Checks if the instrument is a stock.
* @author Torrey Leonard <https://github.com/Ladinn>
* @returns {Boolean}
*/
isStock() {
return this.type === "stock";
}
/**
* Checks if the instrument is an exchange traded product.
* @author Torrey Leonard <https://github.com/Ladinn>
* @returns {Boolean}
*/
isETP() {
return this.type === "etp";
}
/**
* Checks if the instrument is an American Depositary Receipt. Typically applies to foreign companies.
* https://www.investopedia.com/terms/a/adr.asp
* @author Torrey Leonard <https://github.com/Ladinn>
* @returns {Boolean}
*/
isADR() {
return this.type === "adr";
}
/**
* Check whether another instance of Instrument equals this instance.
* @author Torrey Leonard <https://github.com/Ladinn>
* @param {Instrument} otherInstrument
* @returns {Boolean}
*/
equals(otherInstrument) {
return otherInstrument.getSymbol() === this.symbol;
}
}
module.exports = Instrument;