node-red-contrib-alpaca
Version:
Nodes for connecting to the Alpaca stock trading API.
360 lines (329 loc) • 12.2 kB
JavaScript
/*
const ENV_API_KEY = process.env.APCA_API_KEY_ID;
const ENV_API_SECRET = process.env.APCA_API_SECRET_KEY;
const ENV_API_KEY_PAPER = process.env.APCA_API_KEY_ID_PAPER;
const ENV_API_SECRET_PAPER = process.env.APCA_API_SECRET_KEY_PAPER;
*/
/* saving for later
fs.open("settings.json","w",(err,fd)=>{
fs.write(fd,'{"simple-enabled":true}',(err,data)=>{
if(err) throw err;
})
})
*/
module.exports = function(RED) {
var Alpaca = require('@alpacahq/alpaca-trade-api');
var apca_cx = function(node,config){
var auth = RED.nodes.getNode(config.auth);
var cx;
var cx_status={
fill:"grey",
shape:"ring",
text:"null"
};
if(auth.PAPER === "true"){
cx = new Alpaca({
keyId: auth.API_KEY,
secretKey: auth.API_SECRET,
paper: true
});
cx_status.shape = "ring";
}else{
cx = new Alpaca({
keyId: auth.API_KEY,
secretKey: auth.API_SECRET,
paper: false
});
cx_status.shape = "dot";
}
cx_status.fill = (cx ? "green" : "red");
cx_status.text = (cx ? "connected" : "disconnected");
node.status(cx_status);
return cx;
};
function validTopic(msg_topic,config_topic, validTopics){
if(validTopics.includes(config_topic)){
return config_topic;
}
else if(validTopics.includes(msg_topic)
&& config_topic == "msgTopic"){
return msg_topic;
}
else {return null;} //invalid topic
}
function universalAlpacaNode(config) {
RED.nodes.createNode(this,config);
var node = this;
var alpaca_conn = apca_cx(node,config);
const validFunctions = [
"getAccount",
"getAccountConfigurations",
"updateAccountConfigurations",
"getAccountActivities",
"getPortfolioHistory",
"getBars",
"createOrder",
"getOrders",
"getOrder",
"getOrderByClientOrderId",
"replaceOrder",
"cancelOrder",
"cancelAllOrders",
"getPosition",
"getPositions",
"closePosition",
"closeAllPositions",
"lastTrade",
"lastQuote",
"getAssets",
"getAsset",
"getCalendar",
"getWatchlists",
"getWatchlist",
"addWatchlist",
"addToWatchlist",
"updateWatchlist",
"deleteWatchlist",
"deleteFromWatchlist"
];
node.on('input', function(msg) {
msg.request = msg.payload; //store original request
var success = function(resp) {
msg.payload = resp; //supply response object (PAYLOAD)
node.send(msg); }; //send message
var error = function(err) {
msg.payload = err.error.message; //supply error string (PAYLOAD)
msg.error = err.error; //supply error object
node.send(msg); }; //send message
var handleGetBars = function(resp) {
for (var symbol in resp){
for (var bar of resp[symbol]){
let d = new Date(bar.startEpochTime * 1000);
console.log(bar.startEpochTime);
if(config.barISOString) bar.startISOString = d.toISOString();
if(config.barDateString) bar.startDateString = d.toDateString();
if(config.barTimeString) bar.startTimeString = d.toTimeString();
if(config.barLocaleString) bar.startLocaleString = d.toLocaleString();
}
}
success(resp);
};
var topic = validTopic(msg.topic,config.topic,validFunctions);
if(!topic){
error({"error":{"message":"Error - Invalid Function","topic":topic}});
}else if(topic == "getBars"){
alpaca_conn[topic](msg.payload.timeframe || "day", msg.payload.symbol, msg.payload.parameters).then(handleGetBars).catch(error);
// BEGIN WATCHLIST CHECKS --- TODO: separate watchlist stuff into separate node
}else if([ "addWatchlist",
"addToWatchlist",
"updateWatchlist",
"deleteFromWatchlist"].includes(topic)){
// above tests for specific functions that use multiple arguments
let name = msg.payload.name || msg.payload.id;
let ticker = msg.payload.ticker || msg.payload.tickers || msg.symbol || msg.symbols;
ticker = topic == "addWatchlist" ? ticker || [] : ticker || ""; //catch for optional args
alpaca_conn[topic](name, ticker).then(success).catch(error);
}else{
// normal passthru ---- functions that take no input don't seem to mind if they get an input
alpaca_conn[topic](msg.payload).then(success).catch(error);
}
});
}
function socketAlpacaDataV2(config) {
RED.nodes.createNode(this,config);
var node = this;
var socket = RED.nodes.getNode(config.socket);
var data_client = socket.data_client;
var subFor = config.subscribeFor;
var symbol = config.symbol;
var debug = socket.debug;
const validDataTopics = [
"trades",
"quotes",
"bars"
];
var cx_status={
fill:"grey",
shape:"dot",
};
node.status(cx_status);
var updateContext = function(){ //TODO: migrate to the same function defined in config node
if(debug){
node.context().global.set(socket.contextname,data_client.session);
}
};
var subscribe = function(){
let msg = {
"symbol": symbol
};
let functionCalls = {
"trades":"subscribeForTrades",
"quotes":"subscribeForQuotes",
"bars": "subscribeForBars",
};
let functionPayloads = {
"trades":"Subscribed For Trades",
"quotes":"Subscribed For Quotes",
"bars": "Subscribed For Bars",
};
let fnCall = functionCalls[subFor];
let payload = functionPayloads[subFor];
msg.topic = fnCall;
msg.payload = symbol + " - " + payload;
// do the work:
if(!fnCall) return;
data_client[fnCall]([symbol]);
if(config.onSubscribeMsg) {
node.send(msg);
}
updateContext();
//update status
cx_status.fill = "green";
cx_status.text = subFor + ": " + symbol;
node.status(cx_status);
};
var unsubscribe = function(){
let msg = {
"symbol": symbol
};
let functionCalls = {
"trades":"unsubscribeFromTrades",
"quotes":"unsubscribeFromQuotes",
"bars": "unsubscribeFromBars",
};
let functionPayloads = {
"trades":"Unsubscribed From Trades",
"quotes":"Unsubscribed From Quotes",
"bars": "Unsubscribed From Bars",
};
let fnCall = functionCalls[subFor];
let payload = functionPayloads[subFor];
msg.topic = fnCall;
msg.payload = symbol + " - " + payload;
// do the work:
if(!fnCall) return;
data_client[fnCall]([symbol]);
if(config.onSubscribeMsg) {
node.send(msg);
}
updateContext();
cx_status.fill = "grey";
cx_status.text = "";
node.status(cx_status);
};
// not fond of the below statement, but onConnect isn't
// getting called when nodes re-deploy without restart
if(data_client.session.currentState != 'connecting'){
subscribe();
}
data_client.onConnect(function () {
subscribe();
var msg = {
'topic':'onConnect',
'payload':'Connected',
'data_client':data_client,
'config':config
};
cx_status.fill = "green";
cx_status.text = subFor + ": " + symbol;
node.status(cx_status);
if(config.onConnectMsg){
node.send(msg);
}
updateContext();
});
data_client.onError((err) => {
node.error(err, {});
node.send({
"data_client":data_client,
"payload":err
});
cx_status.fill = "red";
node.status(cx_status);
});
data_client.onStockTrade((trade) => {
let msg = {};
msg.topic = "onStockTrade";
msg.payload = trade;
if(trade.Symbol == symbol){
node.send(msg);
}
});
data_client.onStockQuote((quote) => {
let msg = {};
msg.topic = "onStockQuote";
msg.payload = quote;
if(quote.Symbol == symbol){
node.send(msg);
}
});
data_client.onStockBar((bar) => {
let msg = {};
msg.topic = "onStockBar";
msg.payload = bar;
if(bar.Symbol == symbol){
node.send(msg);
}
});
data_client.onStateChange((state) => {
let msg = {};
msg.topic = "onStateChange";
msg.payload = state;
if(config.onStateMsg) {
node.send(msg);
}
updateContext();
});
data_client.onDisconnect(() => {
var msg = {
'topic':'onDisconnect',
'payload':'Disconnected'};
if(config.onDisconnectMsg) {
node.send(msg);
}
cx_status.fill = "grey";
node.status(cx_status);
updateContext();
});
node.on("close",unsubscribe);
node.on("input", function(msg){
let new_symbol = msg.payload;
let new_subFor = msg.topic;
unsubscribe();
if (validDataTopics.includes(new_subFor)){
subFor = new_subFor;
}else if(new_subFor != "" && !!new_subFor){
node.error("Invalid Topic: '" + new_subFor
+ "' Use: " + validDataTopics.join(", "));
}
symbol=new_symbol;
subscribe();
});
}
RED.nodes.registerType("alpaca-websocket", function(n) {
RED.nodes.createNode(this,n);
this.wsauth = n.wsauth;
this.feed = n.feed;
this.debug = n.debug;
this.contextname = n.contextname;
var auth = RED.nodes.getNode(this.wsauth);
var alpaca_conn = new Alpaca({
keyId: auth.API_KEY,
secretKey: auth.API_SECRET,
feed: this.feed || "iex"
});
this.updateContext = function(){
if(this.debug){
this.context().global.set(this.contextname,this.data_client.session);
}
};
this.data_client = alpaca_conn.data_stream_v2;
this.on('close', function(){
this.data_client.disconnect();
});
this.data_client.connect();
this.updateContext();
});
RED.nodes.registerType("Alpaca",universalAlpacaNode);
RED.nodes.registerType("Alpaca-Websocket",socketAlpacaDataV2);
};