robinhood-nodets
Version:
Comprehensive TypeScript API wrapper for the Robinhood private API
356 lines • 13.1 kB
JavaScript
import fetch from "node-fetch";
import { robinhoodApiBaseUrl, cryptoApiBaseUrl, endpoints, clientId, } from "./constants.js";
export default class RobinhoodApi {
constructor(authToken) {
this.authToken = authToken;
this.headers = {
Authorization: `Bearer ${authToken}`,
"Content-Type": "application/json;charset=UTF-8",
};
}
async accounts() {
const url = robinhoodApiBaseUrl + endpoints.accounts;
const params = new URLSearchParams();
return this._fetchList(url, params);
}
async user() {
const url = robinhoodApiBaseUrl + endpoints.user;
const params = new URLSearchParams();
return this._fetch(url, params);
}
async dividends() {
const url = robinhoodApiBaseUrl + endpoints.dividends;
const params = new URLSearchParams();
return this._fetchList(url, params);
}
async transfers() {
const url = robinhoodApiBaseUrl + endpoints.ach_transfers;
const params = new URLSearchParams();
return this._fetchList(url, params);
}
async earnings(options) {
let url = robinhoodApiBaseUrl + endpoints.earnings;
if (options.instrument) {
url += "?instrument=" + options.instrument;
}
else if (options.symbol) {
url += "?symbol=" + options.symbol;
}
else {
url += "?range=" + (options.range || 1) + "day";
}
return await this._fetchList(url, new URLSearchParams());
}
async orders(options) {
const url = robinhoodApiBaseUrl + endpoints.orders;
const params = new URLSearchParams();
if (options) {
if (options.fetchAfter) {
params.set("updated_at[gte]", options.fetchAfter);
}
}
return this._fetchList(url, params, options.fetchMaxPages);
}
async positions() {
const url = robinhoodApiBaseUrl + endpoints.positions;
const params = new URLSearchParams();
return this._fetchList(url, params);
}
async nonzero_positions() {
const url = robinhoodApiBaseUrl + endpoints.positions;
const params = new URLSearchParams();
params.set("nonzero", "true");
return this._fetchList(url, params);
}
async crypto_holdings() {
const url = cryptoApiBaseUrl + endpoints.crypto_holdings;
const params = new URLSearchParams();
return this._fetchList(url, params);
}
async place_buy_order(options) {
options.side = "buy";
return this._place_order(options);
}
async place_sell_order(options) {
options.side = "sell";
return this._place_order(options);
}
async _place_order(options) {
const body = JSON.stringify({
account: this.account,
instrument: options.instrument.url,
price: options.bid_price,
quantity: options.quantity,
side: options.side,
symbol: options.instrument.symbol.toUpperCase(),
time_in_force: options.time || "gfd",
trigger: options.trigger || "immediate",
type: options.type || "market",
market_hours: options.market_hours || "regular_hours",
order_form_version: 6,
});
const response = await fetch(robinhoodApiBaseUrl + endpoints.orders, {
method: "POST",
headers: this.headers,
body,
});
if (!response.ok) {
throw new Error("Failed to place order: " +
response.status +
" " +
JSON.stringify(response.json()));
}
return (await response.json());
}
async fundamentals(ticker) {
return this._fetch(robinhoodApiBaseUrl +
endpoints.fundamentals +
String(ticker).toUpperCase() +
"/");
}
async popularity(symbol) {
const quote = await this.quote_data(symbol);
const symbol_uuid = quote[0].instrument.split("/")[4];
return this._fetch(robinhoodApiBaseUrl + endpoints.instruments + symbol_uuid + "/popularity/");
}
async quote_data(symbol) {
const symbols = Array.isArray(symbol) ? symbol.join(",") : symbol;
return this._fetchList(robinhoodApiBaseUrl +
endpoints.quotes +
`?symbols=${symbols.toUpperCase()}`);
}
async investment_profile() {
return this._fetch(robinhoodApiBaseUrl + endpoints.investment_profile);
}
async instruments_by_id(id) {
return this._fetch(robinhoodApiBaseUrl + endpoints.instruments + id + "/");
}
async instruments(symbol) {
return this._fetchList(robinhoodApiBaseUrl +
endpoints.instruments +
`?query=${symbol.toUpperCase()}`);
}
async cancel_order(order) {
let cancelUrl;
if (typeof order === "string") {
cancelUrl =
robinhoodApiBaseUrl + endpoints.cancel_order + order + "/cancel/";
}
else if (order.cancel) {
cancelUrl = order.cancel;
}
if (cancelUrl) {
try {
await this._post(cancelUrl, null);
return true;
}
catch (error) {
return false;
}
}
else {
if (typeof order === "string") {
throw new Error("Order cannot be cancelled.");
}
else {
if (order.state === "cancelled") {
throw new Error("Order already cancelled.");
}
else {
throw new Error("Order cannot be cancelled.");
}
}
}
}
async watchlists() {
return this._fetchList(robinhoodApiBaseUrl + endpoints.watchlists);
}
async create_watch_list(name) {
return this._post(robinhoodApiBaseUrl + endpoints.watchlists, { name });
}
async sp500_up() {
return this._fetchList(robinhoodApiBaseUrl + endpoints.sp500_up);
}
async sp500_down() {
return this._fetchList(robinhoodApiBaseUrl + endpoints.sp500_down);
}
async splits(instrument) {
return this._fetch(robinhoodApiBaseUrl +
endpoints.instruments +
"/" +
instrument +
"/splits/");
}
async historicals(symbol, intv, span) {
return this._fetch(robinhoodApiBaseUrl +
endpoints.quotes +
"historicals/" +
symbol +
"/?interval=" +
intv +
"&span=" +
span);
}
async url(url) {
const response = await fetch(url, {
headers: this.headers,
});
if (!response.ok) {
throw new Error("Failed to fetch URL: " + JSON.stringify(response));
}
return response.json();
}
async news(symbol) {
return this._fetch(robinhoodApiBaseUrl + endpoints.news + "/" + symbol);
}
async tag(tag) {
return this._fetch(robinhoodApiBaseUrl + endpoints.tag + tag);
}
async get_currency_pairs() {
return this._fetch(cryptoApiBaseUrl + endpoints.currency_pairs);
}
async get_crypto(symbol) {
const currencyPairs = await this.get_currency_pairs();
const assets = currencyPairs.results;
const asset = assets.find((a) => a.asset_currency.code.toLowerCase() === symbol.toLowerCase());
if (!asset) {
const codes = assets.map((a) => a.asset_currency.code);
throw new Error("Symbol not found. Only these codes are allowed: " +
JSON.stringify(codes));
}
const response = await fetch(robinhoodApiBaseUrl + endpoints.crypto + asset.id + "/", {
headers: this.headers,
});
if (!response.ok) {
throw new Error("Failed to fetch crypto data: " + JSON.stringify(response));
}
return response.json();
}
async options_positions() {
return this._fetch(robinhoodApiBaseUrl + endpoints.options_positions);
}
async options_orders() {
return this._fetch(robinhoodApiBaseUrl + endpoints.options_orders);
}
async options_dates(symbol) {
const instruments = await this.instruments(symbol);
const tradable_chain_id = instruments[0].tradable_chain_id;
return this._fetch(robinhoodApiBaseUrl + endpoints.options_chains + "/" + tradable_chain_id);
}
async options_available(chain_id, expiration_date, type = "put") {
return this._fetch(robinhoodApiBaseUrl +
endpoints.options_instruments +
`?chain_id=${chain_id}&type=${type}&expiration_date=${expiration_date}&state=active&tradability=tradable`);
}
async expire_token() {
const response = await fetch(robinhoodApiBaseUrl + endpoints.logout, {
method: "POST",
headers: this.headers,
body: JSON.stringify({
client_id: clientId,
token: this.refreshToken,
}),
});
if (!response.ok) {
throw new Error("Failed to expire token: " + JSON.stringify(response));
}
return response.json();
}
async set_account() {
const accounts = await this.accounts();
if (accounts.length > 0) {
this.account = accounts[0].url;
}
}
async refresh_token({ refreshToken, deviceToken, }) {
// Remove any existing Authorization header as shown in the Python code
const headers = {
"Content-Type": "application/x-www-form-urlencoded", // Changed to form-urlencoded
};
// Convert payload to URLSearchParams as it should be form data, not JSON
const payload = new URLSearchParams({
grant_type: "refresh_token",
refresh_token: refreshToken,
client_id: clientId,
scope: "internal",
expires_in: "86400",
device_token: deviceToken,
});
try {
const response = await fetch(robinhoodApiBaseUrl + endpoints.login, {
method: "POST",
headers: headers,
body: payload,
});
if (!response.ok) {
const body = await response.text();
throw new Error(`Failed to refresh token: ${response.status} ${response.statusText}, Body: ${body}`);
}
const data = await response.json();
return { device_token: deviceToken, ...data };
}
catch (error) {
console.error("Error refreshing token:", error);
throw error;
}
}
async _post(url, body) {
const request = {
method: "POST",
headers: this.headers,
};
if (body != null && Object.keys(body).length > 0) {
request.body = JSON.stringify(body);
}
const response = await fetch(url, request);
if (!response.ok) {
throw new Error(`Failed to post data: ${response.status} ${response.statusText}, Body: ${JSON.stringify(await response.text())}`);
}
return (await response.json());
}
/**
* Fetches data from the API.
* @param url The URL to fetch the data from.
* @param params The parameters to pass to the API.
* @returns A promise that resolves to the data.
*/
async _fetch(url, params) {
let urlWithParams = url;
const paramsString = params?.toString();
if (paramsString) {
urlWithParams += `?${paramsString}`;
}
const response = await fetch(urlWithParams, {
method: "GET",
headers: this.headers,
});
if (!response.ok) {
throw new Error(`Failed to fetch data: ${response.status} ${response.statusText}, Body: ${JSON.stringify(await response.text())}`);
}
return (await response.json());
}
/**
* Fetches a list of items from the API.
* @param url The URL to fetch the list from.
* @param params The parameters to pass to the API.
* @param fetchMaxPages The maximum number of pages to fetch. If not provided, only the first page will be fetched.
* @returns A promise that resolves to an array of items.
*/
async _fetchList(url, params, fetchMaxPages) {
const response = await this._fetch(url, params);
let data = response.results;
let nextPage = response.next;
let pagesFetched = 0;
if (fetchMaxPages === undefined) {
return data;
}
while (nextPage && pagesFetched < fetchMaxPages) {
const nextResponse = await this._fetch(nextPage, new URLSearchParams());
data = data.concat(nextResponse.results);
nextPage = nextResponse.next;
pagesFetched++;
}
return data;
}
}
//# sourceMappingURL=api.js.map