UNPKG

robinhood-nodets

Version:

Comprehensive TypeScript API wrapper for the Robinhood private API

356 lines 13.1 kB
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