@alpacahq/alpaca-trade-api
Version:
Javascript library for the Alpaca Trade API
570 lines (530 loc) • 19.5 kB
JavaScript
class LongShort {
constructor(API_KEY,API_SECRET){
this.alpaca = new AlpacaCORS({
keyId: API_KEY,
secretKey: API_SECRET,
baseUrl: 'https://paper-api.alpaca.markets'
});
this.allStocks = ['DOMO', 'TLRY', 'SQ', 'MRO', 'AAPL', 'GM', 'SNAP', 'SHOP', 'SPLK', 'BA', 'AMZN', 'SUI', 'SUN', 'TSLA', 'CGC', 'SPWR', 'NIO', 'CAT', 'MSFT', 'PANW', 'OKTA', 'TWTR', 'TM', 'RTN', 'ATVI', 'GS', 'BAC', 'MS', 'TWLO', 'QCOM'];
// Format the allStocks variable for use in the class.
var temp = [];
this.allStocks.forEach((stockName) => {
temp.push({name: stockName, pc: 0});
});
this.allStocks = temp.slice();
this.long = [];
this.short = [];
this.qShort = null;
this.qLong = null;
this.adjustedQLong = null;
this.adjustedQShort = null;
this.blacklist = new Set();
this.longAmount = 0;
this.shortAmount = 0;
this.timeToClose = null;
this.marketChecker = null;
this.spin = null;
this.chart = null;
this.chart_data = [];
this.positions = [];
}
async run(){
// First, cancel any existing orders so they don't impact our buying power.
var orders;
await this.alpaca.getOrders({
status: "open",
direction: "desc"
}).then((resp) => {
orders = resp;
}).catch((err) => {writeToEventLog(err);});
var promOrders = [];
orders.forEach((order) => {
promOrders.push(new Promise(async (resolve,reject) => {
this.alpaca.cancelOrder(order.id).catch((err) => {writeToEventLog(err);});
resolve();
}));
});
await Promise.all(promOrders);
// Wait for market to open.
writeToEventLog("Waiting for market to open...");
var promMarket = this.awaitMarketOpen();
await promMarket;
writeToEventLog("Market opened.");
// Rebalance the portfolio every minute, making necessary trades.
this.spin = setInterval(async () => {
// Figure out when the market will close so we can prepare to sell beforehand.
await this.alpaca.getClock().then((resp) =>{
var closingTime = new Date(resp.next_close.substring(0,resp.next_close.length - 6));
var currTime = new Date(resp.timestamp.substring(0,resp.timestamp.length - 6));
this.timeToClose = Math.abs(closingTime - currTime);
}).catch((err) => {writeToEventLog(err);});
if(this.timeToClose < (60000 * 15)) {
// Close all positions when 15 minutes til market close.
writeToEventLog("Market closing soon. Closing positions.");
await this.alpaca.getPositions().then(async (resp) => {
var promClose = [];
resp.forEach((position) => {
promClose.push(new Promise(async (resolve,reject) => {
var orderSide;
if(position.side == 'long') orderSide = 'sell';
else orderSide = 'buy';
var quantity = Math.abs(position.qty);
await this.submitOrder(quantity,position.symbol,orderSide);
resolve();
}));
});
await Promise.all(promClose);
}).catch((err) => {writeToEventLog(err);});
clearInterval(this.spin);
writeToEventLog("Sleeping until market close (15 minutes).");
setTimeout(() => {
// Run script again after market close for next trading day.
this.run();
}, 60000*15);
}
else {
// Rebalance the portfolio.
await this.rebalance();
this.updateChart();
}
}, 60000);
}
// Spin until the market is open.
awaitMarketOpen(){
var prom = new Promise(async (resolve, reject) => {
var isOpen = false;
await this.alpaca.getClock().then(async (resp) => {
if(resp.is_open) {
resolve();
}
else {
this.marketChecker = setInterval(async () => {
this.updateChart();
await this.alpaca.getClock().then((resp) => {
isOpen = resp.is_open;
if(isOpen) {
clearInterval(this.marketChecker);
resolve();
}
else {
var openTime = new Date(resp.next_open.substring(0, resp.next_close.length - 6));
var currTime = new Date(resp.timestamp.substring(0, resp.timestamp.length - 6));
this.timeToClose = Math.floor((openTime - currTime) / 1000 / 60);
writeToEventLog(this.timeToClose + " minutes til next market open.");
}
}).catch((err) => {writeToEventLog(err);});
}, 60000);
}
});
});
return prom;
}
// Rebalance our position after an update.
async rebalance(){
await this.rerank();
// Clear existing orders again.
var orders;
await this.alpaca.getOrders({
status: 'open',
direction: 'desc'
}).then((resp) => {
orders = resp;
}).catch((err) => {writeToEventLog(err);});
var promOrders = [];
orders.forEach((order) => {
promOrders.push(new Promise(async (resolve, reject) => {
await this.alpaca.cancelOrder(order.id).catch((err) => {writeToEventLog(err);});
resolve();
}));
});
await Promise.all(promOrders);
writeToEventLog("We are taking a long position in: " + this.long.toString());
writeToEventLog("We are taking a short position in: " + this.short.toString());
// Remove positions that are no longer in the short or long list, and make a list of positions that do not need to change. Adjust position quantities if needed.
var positions;
await this.alpaca.getPositions().then((resp) => {
positions = resp;
}).catch((err) => {writeToEventLog(err);});
var promPositions = [];
var executed = {long:[], short:[]};
var side;
this.blacklist.clear();
positions.forEach((position) => {
promPositions.push(new Promise(async (resolve, reject) => {
if(this.long.indexOf(position.symbol) < 0){
// Position is not in long list.
if(this.short.indexOf(position.symbol) < 0){
// Position not in short list either. Clear position.
if(position.side == "long") side = "sell";
else side = "buy";
var promCO = this.submitOrder(Math.abs(position.qty), position.symbol, side);
await promCO.then(() => {resolve();});
}
else{
// Position in short list.
if(position.side == "long") {
// Position changed from long to short. Clear long position and short instead
var promCS = this.submitOrder(position.qty, position.symbol, "sell");
await promCS.then(() => {resolve();});
}
else {
if(Math.abs(position.qty) == this.qShort){
// Position is where we want it. Pass for now.
}
else{
// Need to adjust position amount
var diff = Number(Math.abs(position.qty)) - Number(this.qShort);
if(diff > 0){
// Too many short positions. Buy some back to rebalance.
side = "buy"
}
else{
// Too little short positions. Sell some more.
side = "sell"
}
var promRebalance = this.submitOrder(Math.abs(diff), position.symbol, side);
await promRebalance;
}
executed.short.push(position.symbol);
this.blacklist.add(position.symbol);
resolve();
}
}
}
else{
// Position in long list.
if(position.side == "short"){
// Position changed from short to long. Clear short position and long instead.
var promCS = this.submitOrder(Math.abs(position.qty), position.symbol, "buy");
await promCS.then(() => {resolve();});
}
else{
if(position.qty == this.qLong){
// Position is where we want it. Pass for now.
}
else{
// Need to adjust position amount.
var diff = Number(position.qty) - Number(this.qLong);
if(diff > 0){
// Too many long positions. Sell some to rebalance.
side = "sell";
}
else{
// Too little long positions. Buy some more.
side = "buy";
}
var promRebalance = this.submitOrder(Math.abs(diff), position.symbol, side);
await promRebalance;
}
executed.long.push(position.symbol);
this.blacklist.add(position.symbol);
resolve();
}
}
}));
});
await Promise.all(promPositions);
// Send orders to all remaining stocks in the long and short list.
var promLong = this.sendBatchOrder(this.qLong, this.long, 'buy');
var promShort = this.sendBatchOrder(this.qShort, this.short, 'sell');
var promBatches = [];
this.adjustedQLong = -1;
this.adjustedQShort = -1;
await Promise.all([promLong, promShort]).then(async (resp) => {
// Handle rejected/incomplete orders.
resp.forEach(async (arrays, i) => {
promBatches.push(new Promise(async (resolve, reject) => {
if(i == 0) {
arrays[1] = arrays[1].concat(executed.long);
executed.long = arrays[1].slice();
}
else {
arrays[1] = arrays[1].concat(executed.short);
executed.short = arrays[1].slice();
}
// Return orders that didn't complete, and determine new quantities to purchase.
if(arrays[0].length > 0 && arrays[1].length > 0){
var promPrices = this.getTotalPrice(arrays[1]);
await Promise.all(promPrices).then((resp) => {
var completeTotal = resp.reduce((a, b) => a + b, 0);
if(completeTotal != 0){
if(i == 0){
this.adjustedQLong = Math.floor(this.longAmount / completeTotal);
}
else{
this.adjustedQShort = Math.floor(this.shortAmount / completeTotal);
}
}
});
}
resolve();
}));
});
await Promise.all(promBatches);
}).then(async () => {
// Reorder stocks that didn't throw an error so that the equity quota is reached.
var promReorder = new Promise(async (resolve, reject) => {
var promLong = [];
if(this.adjustedQLong >= 0){
this.qLong = this.adjustedQLong - this.qLong;
executed.long.forEach(async (stock) => {
promLong.push(new Promise(async (resolve, reject) => {
var promLong = this.submitOrder(this.qLong, stock, 'buy');
await promLong;
resolve();
}));
});
}
var promShort = [];
if(this.adjustedQShort >= 0){
this.qShort = this.adjustedQShort - this.qShort;
executed.short.forEach(async(stock) => {
promShort.push(new Promise(async (resolve, reject) => {
var promShort = this.submitOrder(this.qShort, stock, 'sell');
await promShort;
resolve();
}));
});
}
var allProms = promLong.concat(promShort);
if(allProms.length > 0){
await Promise.all(allProms);
}
resolve();
});
await promReorder;
});
}
// Re-rank all stocks to adjust longs and shorts.
async rerank(){
await this.rank();
// Grabs the top and bottom quarter of the sorted stock list to get the long and short lists.
var longShortAmount = Math.floor(this.allStocks.length / 4);
this.long = [];
this.short = [];
for(var i = 0; i < this.allStocks.length; i++){
if(i < longShortAmount) this.short.push(this.allStocks[i].name);
else if(i > (this.allStocks.length - 1 - longShortAmount)) this.long.push(this.allStocks[i].name);
else continue;
}
// Determine amount to long/short based on total stock price of each bucket.
var equity;
await this.alpaca.getAccount().then((resp) => {
equity = resp.equity;
}).catch((err) => {writeToEventLog(err);});
this.shortAmount = 0.30 * equity;
this.longAmount = Number(this.shortAmount) + Number(equity);
var promLong = await this.getTotalPrice(this.long);
var promShort = await this.getTotalPrice(this.short);
var longTotal;
var shortTotal;
await Promise.all(promLong).then((resp) => {
longTotal = resp.reduce((a, b) => a + b, 0);
});
await Promise.all(promShort).then((resp) => {
shortTotal = resp.reduce((a, b) => a + b, 0);
});
this.qLong = Math.floor(this.longAmount / longTotal);
this.qShort = Math.floor(this.shortAmount / shortTotal);
}
// Get the total price of the array of input stocks.
getTotalPrice(stocks){
var proms = [];
stocks.forEach(async (stock) => {
proms.push(new Promise(async (resolve, reject) => {
await this.alpaca.getBars('minute', stock, {limit: 1}).then((resp) => {
resolve(resp[stock][0].c);
}).catch((err) => {writeToEventLog(err);});
}));
});
return proms;
}
// Submit an order if quantity is above 0.
async submitOrder(quantity, stock, side){
var prom = new Promise(async (resolve, reject) => {
if(quantity > 0){
await this.alpaca.createOrder({
symbol: stock,
qty: quantity,
side: side,
type: 'market',
time_in_force: 'day',
}).then(() => {
writeToEventLog("Market order of |" + quantity + " " + stock + " " + side + "| completed.");
resolve(true);
}).catch((err) => {
writeToEventLog("Order of |" + quantity + " " + stock + " " + side + "| did not go through.");
resolve(false);
});
}
else {
writeToEventLog("Quantity is <= 0, order of |" + quantity + " " + stock + " " + side + "| not sent.");
resolve(true);
}
});
return prom;
}
// Submit a batch order that returns completed and uncompleted orders.
async sendBatchOrder(quantity, stocks, side){
var prom = new Promise(async (resolve, reject) => {
var incomplete = [];
var executed = [];
var promOrders = [];
stocks.forEach(async (stock) => {
promOrders.push(new Promise(async (resolve, reject) => {
if(!this.blacklist.has(stock)){
var promSO = this.submitOrder(quantity, stock, side);
await promSO.then((resp) => {
if(resp) executed.push(stock);
else incomplete.push(stock);
resolve();
});
}
else resolve();
}));
});
await Promise.all(promOrders).then(() => {
resolve([incomplete, executed]);
});
});
return prom;
}
// Get percent changes of the stock prices over the past 10 minutes.
getPercentChanges(allStocks){
var length = 10;
var promStocks = [];
allStocks.forEach((stock) => {
promStocks.push(new Promise(async (resolve, reject) => {
await this.alpaca.getBars('minute', stock.name, {limit: length}).then((resp) => {
stock.pc = (resp[stock.name][length - 1].c - resp[stock.name][0].o) / resp[stock.name][0].o;
}).catch((err) => {writeToEventLog(err);});
resolve();
}));
});
return promStocks;
}
// Mechanism used to rank the stocks, the basis of the Long-Short Equity Strategy.
async rank(){
// Ranks all stocks by percent change over the past 10 minutes (higher is better).
var promStocks = this.getPercentChanges(this.allStocks);
await Promise.all(promStocks);
// Sort the stocks in place by the percent change field (marked by pc).
this.allStocks.sort((a, b) => {return a.pc - b.pc;});
}
kill() {
clearInterval(this.marketChecker);
clearInterval(this.spin);
throw new error("Killed script");
}
async init() {
var prom = this.getTodayOpenClose();
await prom.then((resp) => {
this.chart = new Chart(document.getElementById("main_chart"), {
type: 'line',
data: {
datasets: [{
label: "equity",
data: []
}]
},
options: {
scales: {
xAxes: [{
type: 'time',
time: {
unit: 'hour',
min: resp[0],
max: resp[1]
},
}],
yAxes: [{
}],
},
title: {
display: true,
text: "Equity"
},
}
});
this.updateChart();
});
}
updateChart() {
this.alpaca.getAccount().then((resp) => {
this.chart.data.datasets[0].data.push({
t: new Date(),
y: resp.equity
});
this.chart.update();
});
this.updateOrders();
this.updatePositions();
}
getTodayOpenClose() {
return new Promise(async (resolve,reject) => {
await this.alpaca.getClock().then(async (resp) => {
await this.alpaca.getCalendar({
start: resp.timestamp,
end: resp.timestamp
}).then((resp) => {
var openTime = resp[0].open;
var closeTime = resp[0].close;
var calDate = resp[0].date;
openTime = openTime.split(":");
closeTime = closeTime.split(":");
calDate = calDate.split("-");
var offset = new Date(new Date().toLocaleString('en-US',{timeZone: 'America/New_York'})).getHours() - new Date().getHours();
openTime = new Date(calDate[0],calDate[1]-1,calDate[2],openTime[0]-offset,openTime[1]);
closeTime = new Date(calDate[0],calDate[1]-1,calDate[2],closeTime[0]-offset,closeTime[1]);
resolve([openTime,closeTime]);
});
});
});
}
updatePositions() {
$("#positions-log").empty();
this.alpaca.getPositions().then((resp) => {
resp.forEach((position) => {
$("#positions-log").prepend(
`<div class="position-inst">
<p class="position-fragment">${position.symbol}</p>
<p class="position-fragment">${position.qty}</p>
<p class="position-fragment">${position.side}</p>
<p class="position-fragment">${position.unrealized_pl}</p>
</div>`
);
})
})
}
updateOrders() {
$("#orders-log").empty();
this.alpaca.getOrders({
status: "open"
}).then((resp) => {
resp.forEach((order) => {
$("#orders-log").prepend(
`<div class="order-inst">
<p class="order-fragment">${order.symbol}</p>
<p class="order-fragment">${order.qty}</p>
<p class="order-fragment">${order.side}</p>
<p class="order-fragment">${order.type}</p>
</div>`
);
})
})
}
}
function runScript(){
var API_KEY = $("#api-key").val();
var API_SECRET = $("#api-secret").val();
var ls = new LongShort(API_KEY,API_SECRET);
ls.init();
ls.run();
}
function killScript(){
$("#event-log").html("Killing script.");
ls.kill();
}
function writeToEventLog(text) {
$("#event-log").prepend(`<p class="event-fragment">${text}</p>`)
}