Idea
Why is it so hard to get into the stock market? There are so many acronyms and complex words that make it difficult to get into the legal gambling sphere that is trading stocks. Seeing this, we decided to create an installation that simplified the whole process of buying and selling stocks.
Implementation
The implementation of this project had 3 sides: the backend where buy and sell orders were created by interacting with the trading API, the display in p5JS where choices were presented to the user, and the Arduino-based controls where the user can manipulate purchase and sales.
The backend
Using Nodejs and the Alpaca Trading API, we were able to create multiple endpoints that allowed us to send and receive data.
Endpoints
POST /buy_orders -> make a buy order for a stock
Arguments
- ticker (string) -> the ticker of the stock to make a purchase of
Returns
- order (Alpaca Order Object) -> an object representing the order made
POST /sell_all -> liquidate all stocks and close all open positions
Arguments – None
Returns – Status 200 if successful
GET /timeline -> current equity trends, percent loss and gain
Arguments – None
Returns
- equity (Number[]) -> list of numbers of equity through the past 24 hours
-
profit_loss (number) -> gain or loss in USD
-
profit_loss_pct (number) -> gain or loss in percentage
-
start_val (number) -> value of initial equity value, used to measure profit loss
GET /rand_tickers -> get two random tickers to choose from (NYSE)
Arguments – None
Returns
- tickers (string[2]) -> list of 2 different tickers
The implementation goes as follows
import express from 'express' import cors from 'cors' const Alpaca = require('@alpacahq/alpaca-trade-api') const fs = require('fs') require('dotenv').config() const app = express() const port = 8000 const stocklist = []; let alpaca; if(process.env.ENV==='DEVELOPMENT'){ alpaca = new Alpaca({ keyId: process.env.TEST_ALPACA_KEY_ID, secretKey: process.env.TEST_ALPACA_SECRET_KEY, paper: true, }) }else if(process.env.ENV==='PRODUCTION'){ alpaca = new Alpaca({ keyId: process.env.ALPACA_KEY_ID, secretKey: process.env.ALPACA_SECRET_KEY, paper: false, }) }else{ throw new Error("Error: Environment Not Properly Selected") } //read stocklist fs.readFile('stocklist.txt', function(err, data) { if(err) throw err; var array = data.toString().split("\n"); for(const stock of array) { const [symbol, name] = stock.split(' ') if(symbol&&name){ let currstock ={ symbol, name:name.replace("\"","") } stocklist.push(currstock) } } }); app.use(express.json()) app.use(cors()) app.post('/buy_order', async(req, res)=>{ try{ const {ticker} = req.body; const order = await alpaca.createOrder({ symbol:ticker, qty:1, side: 'buy', type:'market', time_in_force:'day' }) res.json({order}) }catch(e){ console.log(e) res.status(400).send('err') } }) app.post('/sell_all', async(req, res)=>{ try{ await alpaca.closeAllPositions(); res.json({'success':'success'}) }catch(e){ console.log(e) } }) app.get('/timeline', async(req, res)=>{ try{ const history = await alpaca.getPortfolioHistory({ period: '1D', timeframe:'1Min' }) const last_elem = history.equity[history.equity.length-1]===null ? Math.max(0, await history.equity.findIndex(e=>e===null)): history.equity.length; const cut_history = { equity:history.equity.slice(0,last_elem), profit_loss: history.profit_loss[last_elem-1], profit_loss_pct: history.profit_loss_pct[last_elem-1], start_val: history.profit_loss[last_elem-1] + history.equity[last_elem-1] }; if(cut_history.profit_loss) res.json({...cut_history}) else res.json({equity:[200,100,50,200],profit_loss:20,profit_loss_pct:0.1}) }catch(e){ console.log(e) res.send('err') } }) app.get('/randtickers', async(req,res)=>{ try{ const rand1 = Math.floor(stocklist.length * Math.random()); let rand2 = Math.floor(stocklist.length * Math.random()); while(rand2===rand1){ rand2 = Math.floor(stocklist.length * Math.random()); } res.json({tickers: [stocklist[rand1],stocklist[rand2]]}) }catch(e){ res.status(400).send('err') } }) app.listen(port, () => { console.log(`Example app listening on port ${port}`) })
Display
We used p5js for our display, where the user is shown 3 options (2 stocks and 1 skip), as well as their current stock trend. The rotation at the bottom shows the user which option they are hovering over, in addition to the flickering of the background.
To display the equity at the top, we got the data from the API and displayed it on the screen using a p5js shape
function setup() { getTimeline().then(resp=>{ equity = resp.equity; profit_loss = resp.profit_loss profit_loss_pct = resp.profit_loss_pct start_val = resp.start_val }) createCanvas(1440,600); } function plotTimeline(equity){ if(!equity){ return; } const Emin = Math.min(Math.min(...equity), start_val) - 10; const Emax = Math.max(start_val, Math.max(...equity)) + 10; const maxGraphHeight = height/4; const plotEquity = []; push(); noFill(); stroke(0) beginShape(); for(let i =0; i<equity.length; i++){ vertex(map(i,0,equity.length, 10, 5*width/6),map(equity[i], Emin, Emax, 10, maxGraphHeight)) } endShape(); push(); stroke(100,100,100) line(0,map(start_val||equity[0], Emin, Emax, 10, maxGraphHeight),5*width/6,map(start_val||equity[0], Emin, Emax, 10, maxGraphHeight)); pop() pop(); }
To get the ticker data, we got the data from the backend, and set a global object to hold the values
async function getTickers(){ try{ const data = await fetch('http://localhost:8000/randtickers') return data.json() }catch(e){ console.log(e) } } function draw() { background(255); currSelect = Math.min(Math.max(Math.floor(map(potVal, minPVAL, maxPVAL, 0, 3)),0),3) //update every minute (bcuz that's how it works serverside) if(frameCount%FPS*60===0){ getTimeline().then(resp=>{ equity = resp.equity; profit_loss = resp.profit_loss profit_loss_pct = resp.profit_loss_pct start_val = resp.start_val }) } push(); //choice 0 if(currSelect === 0 && Math.floor(frameCount/60)%2===0){ fill(200,100,100) }else{ fill(255,0,0) } rect(0,20+height/4,1*width/3,3*height/4-20) //choice 1 (skip) if(currSelect === 1 && Math.floor(frameCount/60)%2===0){ fill(175,175,175) }else{ fill(100,100,100) } rect(width/3,20+height/4,1*width/3,3*height/4-20) //choice 2 if(currSelect === 2 && Math.floor(frameCount/60)%2===0){ fill(100,100,200) }else{ fill(0,0,255) } rect(2*width/3,20+height/4,1*width/3,3*height/4-20) pop(); push(); fill(255) noStroke(); textSize(24) textAlign(CENTER) text(tickers[0].symbol, width/6, 2*height/4) text(tickers[0].name, width/6, 3*height/4) text("SKIP", 3*width/6, 11*height/16) text(tickers[1].symbol, 5*width/6, 2*height/4) text(tickers[1].name, 5*width/6, 3*height/4) if(flicker){ fill(255); text(`SELECTED${".".repeat(map(frameCount%60,0,59,1,4))}`, (currSelect*2+1)*width/6, 7*height/8) } pop(); plotTimeline(equity); push(); textSize(18); fill(profit_loss<0?255:0,profit_loss>0?255:0,0) text(`${profit_loss?'$'+profit_loss.toString():""}`, 5*width/6+10, height/7) text(`${profit_loss_pct?(profit_loss_pct*100).toString().slice(0,7)+"%":""}`, 5*width/6+10, height/7+20) pop(); push(); if(selling && Math.floor(map(frameCount%60, 0, 59, 0, 2))){ textAlign(CENTER) textSize(50) fill(0) rect(width/4, height/4, width/2, height/2) fill(255,0,0); text('SELLING', width/2, height/2); } pop(); push() fill(0,255,0) noStroke() circle(width/2, height, RAD*2) angleMode(DEGREES) let xdeg = 180- map(potVal, minPVAL, maxPVAL, XOFFSET, 180-XOFFSET) let x = width/2 + cos(xdeg) * XRAD let y = Math.sqrt(XRAD**2 - (x-(width/2))**2) + height let xlrange = [width/2-RAD, width/2-RAD, width/2] let ylrange = [height, height, height - RAD] let xrrange = [width/2, width/2+RAD, width/2+RAD] let yrrange = [height - RAD, height, height] triangle(xlrange[currSelect], ylrange[currSelect], x, height-y+height, xrrange[currSelect], yrrange[currSelect]); pop() }
To handle the orders, we had to connect with the arduino, so in the mean time we implemented the code structure that enabled us to receive data and update the states in the program as necessary.
function readSerial(data) { //////////////////////////////////// //READ FROM ARDUINO HERE //////////////////////////////////// if (data != null) { let fromArduino = split(trim(data), ","); if(fromArduino.length === 3){ if(prevsellbtn == 0 && sellbtn == 1){ selling=true; sellAll().then(()=>selling=false); end_time = frameCount + (FPS*SELLTIME) }else if(prevbuybtn == 0 && buybtn == 1){ if(!flicker){ flicker=true; switch(currSelect){ case 0: makeOrder(tickers[0].symbol) .then(()=>getTickers()) .then(data=>{tickers=data.tickers;flicker=false;}); break; case 1: getTickers() .then(data=>{tickers=data.tickers;flicker=false;}); break case 2: makeOrder(tickers[1].symbol) .then(()=>getTickers()) .then(data=>{tickers=data.tickers;flicker=false;}); break; } } } prevsellbtn = sellbtn; prevbuybtn = buybtn; sellbtn = parseInt(fromArduino[0]) buybtn = parseInt(fromArduino[1]) potVal = maxPVAL - parseInt(fromArduino[2]) ////////////////////////////////// //SEND TO ARDUINO HERE (handshake) ////////////////////////////////// if(end_time && end_time > frameCount){ writeSerial("1\n"); }else{ writeSerial("0\n"); if(frameCount<end_time){ end_time = 0; } } } }}
The idea being that a potentiometer value from arduino would dictate the highlighted choice, and a button would lock the choice in to make a purchase. A user would also be able to close all open positions through a button press, and that would send a positive value to the arduino serial.
Arduino Implementation
As mentioned previously, we needed a potentiometer and two buttons, but we also wanted to add sound and lights for feedback. We loaded two sounds onto a microSD card onto the arduino, one for a sale and one for a purchase, and loaded them as follows along with the LEDs.
#include <SPI.h> #include <Adafruit_VS1053.h> #include <SD.h> #define BREAKOUT_RESET 9 // VS1053 reset pin (output) #define BREAKOUT_CS 10 // VS1053 chip select pin (output) #define BREAKOUT_DCS 8 // VS1053 Data/command select pin (output) #define SHIELD_RESET -1 // VS1053 reset pin (unused!) #define SHIELD_CS 7 // VS1053 chip select pin (output) #define SHIELD_DCS 6 // VS1053 Data/command select pin (output) #define CARDCS 4 // Card chip select pin #define DREQ 3 // VS1053 Data request, ideally an Interrupt pin Adafruit_VS1053_FilePlayer musicPlayer = Adafruit_VS1053_FilePlayer(SHIELD_RESET, SHIELD_CS, SHIELD_DCS, DREQ, CARDCS); void setup() { Serial.begin(9600); pinMode(2, INPUT); pinMode(7, INPUT`); pinMode(1, OUTPUT); pinMode(5, OUTPUT); pinMode(6, OUTPUT); pinMode(4, OUTPUT); pinMode(3, OUTPUT); while (Serial.available() <= 0) { Serial.println("0,0,0"); delay(300); } if (! musicPlayer.begin()) { while (1); } if (!SD.begin(CARDCS)) { while (1); } musicPlayer.setVolume(20,20); musicPlayer.useInterrupt(VS1053_FILEPLAYER_PIN_INT); }
We then parsed the p5js output and sent back the button and potentiometer values.
int SELLPIN = 2; int BUYPIN = 7; int POTPIN = A0; void loop() { if (Serial.available()) { int sellBTN = digitalRead(SELLPIN); int buyBTN = digitalRead(BUYPIN); int sensorValue = analogRead(POTPIN); Serial.print(sellBTN); Serial.print(','); Serial.print(buyBTN); Serial.print(','); Serial.print(sensorValue); Serial.println(""); if(buyBTN){ musicPlayer.playFullFile("/track001.mp3"); } if(sellBTN){ musicPlayer.playFullFile("/track002.mp3"); } byte c = Serial.read(); if(c=='1'){ digitalWrite(1, 1); digitalWrite(5, 1); digitalWrite(6, 1); digitalWrite(4, 1); digitalWrite(3, 1); }else{ digitalWrite(1, 0); digitalWrite(5, 0); digitalWrite(6, 0); digitalWrite(4, 0); digitalWrite(3, 0); } } }
This way, we were able to get the p5js input and send out our own values using the handshake procedure.
The image shows the complete implementation.
Reflections
Overall, the implementation worked well and made for an intuitive program that enabled people to experiment with the stock market. One of the main limitations of our system is that it only functions between 17:30-00:00 GST due to the stock market being open only for those hours. Other than that however, we found that it provided an interactive experience for the user.
We initially wanted to use an airhorn for the selling sound, but we had hardware restrictions, so decided to go for a speaker instead. We had a couple software slip ups too, as out of nowhere during one of our work sessions, my Google Chrome quit and couldn’t be reopened. In addition, some of the ports on the arduino we used were not working, so we had to test out many different ports before the system worked at full capacity.
We’re happy with the project we made in the class, and hope that the users as less afraid of investing. The repository for the backend can be found here. The .env file is not provided, so the user must provide their own API keys.