robinhood-observer
Version:
Comprehensive client featuring RxJS Streams and a CLI for Robinhood Free Stock Trading. A drop in replacement for @aurbano obinhood which includes callback, promise and observable support.
836 lines (779 loc) • 24.8 kB
JavaScript
// import device from "./device.mjs"
const RxJS = require('rxjs');
const Rx = require('rx');
const program = require('commander');
const Promise = require('bluebird');
const _ = require('lodash');
const fs = require('fs');
const pckg = require('../package.json');
const Device = require('./device.js');
const Auth = require('./auth.js');
const endpoints = require('./endpoints');
const config = require('./config');
const Crypto = require('./crypto');
;
/**
* [Robinhood description]
* @param { username: string, password: string } opts [description]
* @param {Function} callback [description]
*/
function Robinhood(opts, callback) {
const api = { test: 'value', crypto: {} };
/* +--------------------------------+ *
* | Internal variables | *
* +--------------------------------+ */
const _apiUrl = 'https://api.robinhood.com/';
const device = new Device();
const auth = new Auth();
const crypto = new Crypto();
const _options = opts || {};
// Private API Endpoints
const _clientId = 'c82SH0WZOsabOXGP2sxqcj34FxkvfnWRZBKlBjFS';
const _isInit = false;
const _private = {
session: {},
account: null,
username: null,
password: null,
headers: null,
auth_token: null,
device_token: null,
};
function _init() {
_private.username = _.has(_options, 'username') ? _options.username : (process.env.ROBINHOOD_USERNAME ? process.env.ROBINHOOD_USERNAME : null);
_private.password = _.has(_options, 'password') ? _options.password : (process.env.ROBINHOOD_PASSWORD ? process.env.ROBINHOOD_PASSWORD : null);
_private.auth_token = _.has(_options, 'token') ? _options.token : (process.env.ROBINHOOD_TOKEN ? process.env.ROBINHOOD_TOKEN : null);
if (!_private.auth_token) {
auth.init(_private, device)
.then(_private => _set_account())
.then(() => crypto.init(auth))
.then((success) => {
callback.call();
})
.catch((err) => {
throw err;
});
} else {
throw new Error('This form of authentication has been deprecated in lieu of using Robinhood 2FA with username, password combo');
}
}
function _set_account() {
return new Promise(((resolve, reject) => {
api.accounts((err, httpResponse, body) => {
if (err) {
reject(err);
}
// Being defensive when user credentials are valid but RH has not approved an account yet
if (
body.results
&& body.results instanceof Array
&& body.results.length > 0
) {
_private.account = body.results[0].url;
}
resolve();
});
}));
}
function options_from_chain({ next, results }) {
return new Promise((resolve, reject) => {
if (!next) return resolve(results);
const tOpts = {
url: next,
};
return auth.get(tOpts, callback)
.then((body) => {
resolve(
options_from_chain({
next: body.next,
results: results.concat(body.results),
}),
);
})
.catch((err) => {
reject(err);
});
});
}
function filter_bad_options(options) {
return options.filter(option => option.tradability == 'tradable');
}
function stitch_options_with_details([details, options]) {
const paired = details.map((detail) => {
const match = options.find(option => option.url == detail.instrument);
if (match) {
Object.keys(match).forEach(key => (detail[key] = match[key]));
}
return detail;
});
// Remove nulls
return paired.filter(item => item);
}
function get_options_details(options) {
const grouped_options = group_options_by_max_per_request(options);
return Promise.all(
grouped_options.map((group) => {
const option_urls = group.map(option => encodeURIComponent(option.url));
const tOpts = {
uri:
`${_apiUrl
+ endpoints.options_marketdata
}?instruments=${
option_urls.join('%2C')}`,
};
return auth.get(tOpts, callback);
}),
).then(options_details => [options_details.flat(), grouped_options.flat()]);
}
const max_options_details_per_request = 17;
function group_options_by_max_per_request(options) {
const filtered = filter_bad_options(options);
const groups = [];
for (
let i = 0;
i < filtered.length - 1;
i += max_options_details_per_request
) {
groups.push(filtered.slice(i, i + max_options_details_per_request));
}
return groups;
}
/* +--------------------------------+ *
* | API observables | *
* +--------------------------------+ */
/**
*
* [observeQuote description]
* @param [string] symbol The Symbol or Array of Symbols you want to observe.
* @param {number} frequency Frequency to poll the Robinhood API in Milliseconds
*
* @return {[Observable]} An observable which updates on the frequency provided.
*
*/
api.observeQuote = function (symbol, frequency) {
symbol = Array.isArray(symbol) ? symbol = symbol.join(',') : symbol;
frequency = frequency || 1800; // Set frequency of updates to 800 by default
const count = 0;
const source = Rx.Observable.create((observer) => {
const intrvl = setInterval(() => {
const tOpts = {
uri: _apiUrl + endpoints.quotes,
qs: { symbols: symbol.toUpperCase() },
};
auth.get(tOpts)
.then((success) => {
observer.onNext(success);
});
}, frequency);
return () => {
clearInterval(intrvl);
};
});
return source;
};
/**
* [observePositions description]
* @param {number} frequency Frequency to poll the Robinhood API in Milliseconds
* @return {Observable} An observable which updates on the frequency provided.
*/
api.observePositions = function (frequency, nonzero = true) {
frequency = frequency || 5000; // Set frequency of updates to 5000 by default
const source = Rx.Observable.create((observer) => {
const intrvl = setInterval(() => {
const tOpts = {
uri: _apiUrl + endpoints.positions + (nonzero ? '?nonzero=true' : ''),
};
auth.get(tOpts)
.then((success) => {
observer.onNext(success);
})
.catch((err) => {
observer.onError(err);
});
}, frequency);
return () => {
clearInterval(intrvl);
};
});
return source;
};
/**
* [observeOrders description]
* @param {number} frequency Frequency to poll the Robinhood API in Milliseconds
* @return {Observable} An observable which updates on the frequency provided.
*/
api.observeOrders = function (frequency) {
frequency = frequency || 5000; // Set frequency of updates to 5000 by default
const source = Rx.Observable.create((observer) => {
const intrvl = setInterval(() => {
const tOpts = {
uri: _apiUrl + endpoints.orders,
};
auth.get(tOpts)
.then((success) => {
observer.onNext(success);
})
.catch((err) => {
observer.onError(err);
});
}, frequency);
return () => {
clearInterval(intrvl);
};
});
return source;
};
/**
* [observeCryptoQuote description]
* @param {number} frequency Frequency to poll the Robinhood API in Milliseconds
* @return {Observable} An observable which updates on the frequency provided.
*/
api.observeCryptoQuote = function (symbol, frequency) {
frequency = frequency || 1800; // Set frequency of updates to 800 by default
const count = 0;
const source = Rx.Observable.create((observer) => {
let intrvl;
api.crypto_init()
.then((success) => {
intrvl = setInterval(() => {
console.log(new Date());
api.crypto_quote(symbol)
.then((success) => {
observer.onNext(success);
})
.catch((err) => {
console.error(err);
});
}, frequency);
})
.catch((err) => {
console.error(err);
return err;
});
return () => {
clearInterval(intrvl);
};
});
return source;
};
/* +--------------------------------+ *
* | Robinhood API methods | *
* +--------------------------------+ */
api.auth_token = function () {
return _private.auth_token;
};
// Invoke robinhood logout. Note: User will need to reintantiate
// this package to get a new token!
api.expire_token = function (callback) {
return auth.post({
uri: _apiUrl + endpoints.logout,
}, callback);
};
/**
* [investment_profile description]
* @param {Function} callback [description]
* @return {Function or Promise} [description]
*/
api.investment_profile = function (callback) {
const tUri = _apiUrl + endpoints.investment_profile;
const tOpts = {
uri: tUri,
};
return auth.get(tOpts, callback);
};
/**
* [fundamentals description]
* @param [string] symbol [description]
* @param {Function} callback [description]
* @return {Function or Promise} [description]
*/
api.fundamentals = function (symbol, callback) {
symbol = Array.isArray(symbol) ? symbol = symbol.join(',') : symbol;
const tUri = _apiUrl + endpoints.fundamentals;
const tOpts = {
uri: tUri,
qs: { symbols: symbol },
};
return auth.get(tOpts, callback);
};
/**
* [instruments description]
* @param [string] symbol [description]
* @param {Function} callback [description]
* @return {[Function or Promise]} [description]
*/
api.instruments = function (symbol, callback) {
symbol = Array.isArray(symbol) ? symbol = symbol.join(',') : symbol;
const tUri = _apiUrl + endpoints.instruments;
const tOpts = {
uri: tUri,
qs: { symbols: symbol.toUpperCase() },
};
return auth.get(tOpts, callback);
};
/**
* [currency description]
* @param [String] symbol [description]
* @param {Function} callback [description]
* @return {[Function or Promise]} [description]
*/
api.crypto_quote = function (symbol, callback) {
const tUri = _apiUrl + endpoints.marketdata_forex_quotes;
var tOpts = {
uri: tUri,
};
symbol = Array.isArray(symbol) ? symbol : [symbol];
filtered = _.filter(api.crypto.pairs, o => (symbol.indexOf(o.symbol) > -1) || (symbol.indexOf(o.symbol.split('-')[0]) > -1));
indexed = _.map(filtered, (o) => {
const index = (symbol.indexOf(o.symbol) > -1) ? symbol.indexOf(o.symbol) : ((symbol.indexOf(o.symbol.split('-')[0]) > -1) ? symbol.indexOf(o.symbol.split('-')[0]) : 0);
o.index = index;
return o;
});
sorted = _.sortBy(indexed, ['index']);
targets = _.map(sorted, item => item.id).join(',');
var tOpts = {
uri: _apiUrl + endpoints.marketdata_forex_quotes,
qs: { ids: targets },
};
return auth.get(tOpts, callback);
};
api.crypto_pairs = function (callback) {
const tOpts = {
uri: `https://nummus.robinhood.com/${endpoints.currency_pairs}`,
headers: {
Host: 'nummus.robinhood.com',
},
};
return auth.get(tOpts, callback);
};
api.crypto_init = function (callback) {
const tUri = `https://nummus.robinhood.com/${endpoints.currency_pairs}`;
return api.crypto_pairs()
.then((success) => {
api.crypto.pairs = success.results;
return success.results;
});
};
/**
* [quote description]
* @param [String] symbol [description]
* @param {Function} callback [description]
* @return {[Function or Promise]} [description]
*/
api.quote = function (symbol, callback) {
const tUri = _apiUrl;
var symbol = Array.isArray(symbol) ? symbol = symbol.join(',') : symbol;
const tOpts = {
uri: _apiUrl + endpoints.quotes,
qs: { symbols: symbol.toUpperCase() },
};
return auth.get(tOpts, callback);
};
api.quote_data = function (sybmol, callback) {
return api.quote(sybmol, callback);
};
/**
* [accounts description]
* @param {Function} callback [description]
* @return {[Function or Promise]} [description]
*/
api.accounts = function (callback) {
const tUri = _apiUrl;
const tOpts = {
uri: _apiUrl + endpoints.accounts,
};
return auth.get(tOpts, callback);
};
/**
* [user description]
* @param {Function} callback [description]
* @return {[Function or Promise]} [description]
*/
api.user = function (callback) {
const tOpts = {
uri: _apiUrl + endpoints.user,
};
return auth.get(tOpts, callback);
};
/**
* [userBasicInfo description]
* @param {Function} callback [description]
* @return {[type]} [description]
*/
api.userBasicInfo = function (callback) {
const tUri = _apiUrl;
const tOpts = {
uri: _apiUrl + endpoints.user_basic_info,
};
return auth.get(tOpts, callback);
};
/**
* [userAdditionalInfo description]
* @param {Function} callback [description]
* @return {[type]} [description]
*/
api.userAdditionalInfo = function (callback) {
const tUri = _apiUrl;
const tOpts = {
uri: _apiUrl + endpoints.user_additional_info,
};
return auth.get(tOpts, callback);
};
/**
* [userEmployment description]
* @param {Function} callback [description]
* @return {[type]} [description]
*/
api.userEmployment = function (callback) {
const tUri = _apiUrl;
const tOpts = {
uri: _apiUrl + endpoints.user_employment,
};
return auth.get(tOpts, callback);
};
/**
* [userInvestmentProfile description]
* @param {Function} callback [description]
* @return {[type]} [description]
*/
api.userInvestmentProfile = function (callback) {
const tUri = _apiUrl;
const tOpts = {
uri: _apiUrl + endpoints.investment_profile,
};
return auth.get(tOpts, callback);
};
/**
* [dividends description]
* @param {Function} callback [description]
* @return {[Function or Promise]} [description]
*/
api.dividends = function (callback) {
const tUri = _apiUrl + endpoints.dividends;
const tOpts = {
uri: tUri,
};
return auth.get(tOpts, callback);
};
/**
* [orders description]
* @param {Function} callback [description]
* @return {[Function or Promise]} [description]
*/
api.orders = function (callback) {
const tUri = _apiUrl + endpoints.orders;
const tOpts = {
uri: tUri,
};
return auth.get(tOpts, callback);
};
/**
* [cancel description]
* @param {[type]} order [description]
* @param {Function} callback [description]
* @return {[Function or Promise]} [description]
*/
api.cancel = function (order, callback) {
if (order && typeof order === 'object' && order.cancel) {
const tUri = _apiUrl + order.cancel;
const tOpts = {
uri: tUri,
};
if (callback && typeof callback === 'function') {
if (order.cancel) {
return auth.post(tOpts, callback);
}
callback({ message: order.state == 'cancelled' ? 'Order already cancelled.' : 'Order cannot be cancelled.', order }, null, null);
} else {
return auth.get(tOpts);
}
} else if (typeof order === 'function') {
order(new Error('An order must be provided.'), null, null);
} else if (callback && typeof callback === 'function') {
callback(new Error('An order must be provided.'), null, null);
} else {
return new Promise(((resolve, reject) => {
setTimeout(() => {
reject(new Error('An order must be provided.'));
});
}));
}
};
/**
* [cancel_order description]
* @param {[type]} order [description]
* @param {Function} callback [description]
* @return {[Function or Promise]} [description]
*/
api.cancel_order = function (order, callback) {
return api.cancel(order, callback);
};
/**
* [_place_order description]
* @param {[type]} options [description]
* @param {Function} callback [description]
* @return {[type]} [description]
*/
const _place_order = function (options, callback) {
const tUri = _apiUrl + endpoints.orders;
// Get instrument url
const tOpts = {
uri: tUri,
form: {
account: _private.account,
instrument: options.instrument.url,
price: options.bid_price,
stop_price: options.stop_price,
quantity: options.quantity,
side: options.transaction,
symbol: options.instrument.symbol.toUpperCase(),
time_in_force: options.time || 'gfd',
trigger: options.trigger || 'immediate',
type: options.type || 'market',
},
};
return auth.post(tOpts, callback);
};
/**
* [place_buy_order description]
* @param {[type]} options [description]
* @param {Function} callback [description]
* @return {[type]} [description]
*/
api.place_buy_order = function (options, callback) {
return api.buy(options, callback);
};
/**
* [buy description]
* @param {[type]} options [description]
* @param {Function} callback [description]
* @return {[type]} [description]
*/
api.buy = function (options, callback) {
// Check if instrument is provided.
// Check if instrument url is provided.
if (options.instrument || options.instrument.url.length > 0) {
if (callback && typeof callback === 'function') {
options.transaction = 'buy';
return _place_order(options, callback);
}
return _place_order(options);
}
// If no instrument is provided, get it.
// If no instrument url is provided, get it.
if (typeof options === 'object') {
// instrument was included but no instrument url provided
if (options.instrument.symbol) {
// Simply get the instrument, append the url and send the buy request
api.instruments(options.instrument.symbol)
.then((result) => {
_.forEach(result.results, (value, key) => {
if (value.symbol == options.instrument.symbol) {
console.log(`Got Instrument for: ${ticker}`);
options.instrument.url = value.url;
return _place_order(options);
}
console.error('Unable to set instrument for order.');
});
})
.catch((err) => {
console.error(err);
});
}
} else if (typeof options === 'string' && typeof callback === 'object') {
// Using alternative syntax api.buy(symbol:String, options:Object)
const symbol = options;
options = callback;
api.instruments(symbol)
.then((result) => {
_.forEach(result.results, (value, key) => {
if (value.symbol == options.instrument.symbol) {
options.instrument.url = value.url;
console.log(`Got Instrument for: ${options}`);
return _place_order(options);
}
console.error('Unable to set instrument for order.');
});
})
.catch((err) => {
console.error(err);
});
} else {
console.log('Invalid request parameters were sent.');
}
};
/**
* [place_sell_order description]
* @param {[type]} options [description]
* @param {Function} callback [description]
* @return {[type]} [description]
*/
api.place_sell_order = function (options, callback) {
return api.sell(options, callback);
};
/**
* [sell description]
* @param {[type]} options [description]
* @param {Function} callback [description]
* @return {[type]} [description]
*/
api.sell = function (options, callback) {
options.transaction = 'sell';
if (callback && typeof callback === 'function') {
return _place_order(options, callback);
}
return _place_order(options);
};
/**
* [positions description]
* @param {Function} callback [description]
* @return {[type]} [description]
*/
api.positions = function (callback) {
const tUri = _apiUrl + endpoints.positions;
const tOpts = {
uri: tUri,
};
return auth.get(tOpts, callback);
};
/**
* [news description]
* @param {[type]} symbol [description]
* @param {Function} callback [description]
* @return {[type]} [description]
*/
api.news = function (symbol, callback) {
const tUri = _apiUrl + [endpoints.news, '/'].join(symbol);
const tOpts = {
uri: tUri,
};
return auth.get(tOpts, callback);
};
/**
* [markets description]
* @param {Function} callback [description]
* @return {[type]} [description]
*/
api.markets = function (callback) {
const tUri = _apiUrl + endpoints.markets;
const tOpts = {
uri: tUri,
};
return auth.get(tOpts, callback);
};
/**
* [sp500_up description]
* @param {Function} callback [description]
* @return {[type]} [description]
*/
api.sp500_up = function (callback) {
const tUri = _apiUrl + endpoints.sp500_up;
const tOpts = {
uri: tUri,
};
return auth.get(tOpts, callback);
};
/**
* [sp500_down description]
* @param {Function} callback [description]
* @return {[type]} [description]
*/
api.sp500_down = function (callback) {
const tUri = _apiUrl + endpoints.sp500_down;
const tOpts = {
uri: tUri,
};
return auth.get(tOpts, callback);
};
/**
* [create_watch_list description]
* @param {[type]} name [description]
* @param {Function} callback [description]
* @return {[type]} [description]
*/
api.create_watch_list = function (name, callback) {
const tUri = _apiUrl + endpoints.watchlists;
const tOpts = {
uri: tUri,
form: {
name,
},
};
return auth.post(tOpts, callback);
};
/**
* [watchlists description]
* @param {Function} callback [description]
* @return {[type]} [description]
*/
api.watchlists = function (callback) {
const tUri = _apiUrl + endpoints.watchlists;
const tOpts = {
uri: tUri,
};
return auth.get(tOpts, callback);
};
/**
* [splits description]
* @param {[type]} instrument [description]
* @param {Function} callback [description]
* @return {[type]} [description]
*/
api.splits = function (instrument, callback) {
const tUri = _apiUrl + [endpoints.instruments, '/splits/'].join(instrument);
const tOpts = {
uri: tUri,
};
return auth.get(tOpts, callback);
};
/**
* [historicals description]
* @param {[type]} symbol [description]
* @param {[type]} intv [description]
* @param {[type]} span [description]
* @param {Function} callback [description]
* @return {[type]} [description]
*/
api.historicals = function (symbol, intv, span, callback) {
if (typeof intv === 'function') {
// callback(new Error("You must provide a symbol, interval and timespan"));
return;
}
const tUri = _apiUrl + [`${endpoints.quotes}historicals/`, `/?interval=${intv}&span=${span}`].join(symbol);
const tOpts = {
uri: tUri,
};
return auth.get(tOpts, callback);
};
/**
* [url description]
* @param {string} url [description]
* @param {Function} callback [description]
* @return {[type]} [description]
*/
api.url = function (url, callback) {
const tOpts = {
uri: url,
};
return auth.get(tOpts, callback);
};
api.init = _init;
api.device = Device;
api.auth = auth;
api.endpoints = endpoints;
api.crypto = crypto;
_init(_options);
return api;
}
if (require.main === module) {
program
.version(pckg.version)
.command('crypto [query]', 'crypto')
.command('crypto [get]', 'crypto')
.command('get [quote]', 'get')
.parse(process.argv);
} else {
console.log('Thanks for using the robinhood-observer cli!');
}
module.exports = Robinhood;