├── .dockerignore ├── src ├── utils │ ├── url.js │ ├── date.js │ └── bsm.js ├── setupTests.js ├── reportWebVitals.js ├── App.js ├── index.js ├── components │ ├── skeletons │ │ ├── tabsSkeleton.jsx │ │ └── quoteSkeleton.jsx │ ├── chain │ │ ├── ivSkewChart.jsx │ │ ├── skeletons │ │ │ ├── selectSkeleton.jsx │ │ │ └── accordionSkeleton.jsx │ │ ├── greeksChart.jsx │ │ ├── expiries.jsx │ │ ├── positions.jsx │ │ ├── editLayout.jsx │ │ ├── optionTable.jsx │ │ ├── editLayoutModal.jsx │ │ ├── option.jsx │ │ └── optionChain.jsx │ ├── navTabs.jsx │ ├── quote.jsx │ ├── analysis │ │ ├── lineChart.jsx │ │ ├── positions.jsx │ │ └── analysis.jsx │ ├── search.jsx │ └── main.jsx └── index.css ├── public ├── vega.png ├── robots.txt └── index.html ├── media ├── demo-1.png └── graphvega-adding-positions.gif ├── docker-compose.yml ├── Dockerfile ├── .github └── workflows │ └── ci.yml ├── server ├── config.js ├── app.js ├── options.js └── stock.js ├── .gitignore ├── LICENSE.md ├── package.json └── README.md /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | -------------------------------------------------------------------------------- /src/utils/url.js: -------------------------------------------------------------------------------- 1 | export const SERVER_URL = 'http://localhost:8000'; -------------------------------------------------------------------------------- /public/vega.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rahuljoshi44/GraphVega/HEAD/public/vega.png -------------------------------------------------------------------------------- /media/demo-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rahuljoshi44/GraphVega/HEAD/media/demo-1.png -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /media/graphvega-adding-positions.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rahuljoshi44/GraphVega/HEAD/media/graphvega-adding-positions.gif -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | services: 3 | app: 4 | build: . 5 | ports: 6 | - '3000:3000' 7 | - '8000:8000' 8 | volumes: 9 | - ./:/var/www 10 | - node_modules:/var/www/node_modules 11 | volumes: 12 | node_modules: -------------------------------------------------------------------------------- /src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:14-alpine AS build-env 2 | 3 | WORKDIR /graphvega 4 | ADD . . 5 | RUN npm ci --only-production 6 | 7 | FROM node:14-alpine 8 | COPY --from=build-env /graphvega /graphvega 9 | 10 | WORKDIR /graphvega 11 | EXPOSE 3000 12 | EXPOSE 8000 13 | ENTRYPOINT ["npm", "start"] 14 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v2 15 | - name: Build 16 | run: | 17 | docker build --tag graphvega:latest . -------------------------------------------------------------------------------- /server/config.js: -------------------------------------------------------------------------------- 1 | const dotenv = require('dotenv'); 2 | dotenv.config(); 3 | 4 | // general config variables available from .env file, defaulting our API base url to 5 | // the sandbox, but you can override this in the .env file if you'd like to use the production endpoint 6 | module.exports = { 7 | API_KEY: process.env.TRADIER_API_KEY, 8 | API_BASE_URL: process.env.TRADIER_API_BASE_URL || 'https://sandbox.tradier.com/v1/' 9 | }; -------------------------------------------------------------------------------- /src/reportWebVitals.js: -------------------------------------------------------------------------------- 1 | const reportWebVitals = onPerfEntry => { 2 | if (onPerfEntry && onPerfEntry instanceof Function) { 3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 4 | getCLS(onPerfEntry); 5 | getFID(onPerfEntry); 6 | getFCP(onPerfEntry); 7 | getLCP(onPerfEntry); 8 | getTTFB(onPerfEntry); 9 | }); 10 | } 11 | }; 12 | 13 | export default reportWebVitals; 14 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import Main from './components/main'; 2 | import './index.css'; 3 | 4 | import { ToastContainer } from 'react-toastify'; 5 | import 'react-toastify/dist/ReactToastify.css' 6 | function App() { 7 | return ( 8 |
9 | 10 |
11 | 12 | 13 |
14 | ); 15 | } 16 | 17 | export default App; 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | .env 21 | 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | .eslintcache 26 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | import reportWebVitals from './reportWebVitals'; 6 | import Bootstrap from "bootstrap/dist/css/bootstrap.css"; 7 | 8 | ReactDOM.render( 9 | 10 | 11 | , 12 | document.getElementById('root') 13 | ); 14 | 15 | // If you want to start measuring performance in your app, pass a function 16 | // to log results (for example: reportWebVitals(console.log)) 17 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 18 | reportWebVitals(); 19 | -------------------------------------------------------------------------------- /src/components/skeletons/tabsSkeleton.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Skeleton from "@material-ui/lab/Skeleton"; 3 | import { 4 | Typography, 5 | Paper, 6 | Grid, 7 | Container 8 | } from "@material-ui/core"; 9 | 10 | const TabsSkeleton = () => { 11 | return ( 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | ); 29 | }; 30 | export default TabsSkeleton; -------------------------------------------------------------------------------- /server/app.js: -------------------------------------------------------------------------------- 1 | const express = require("express") 2 | const path = require("path") 3 | const bcrpyt = require("bcrypt"); 4 | const bodyParser = require("body-parser"); 5 | const cors = require("cors"); 6 | 7 | const app = express(); 8 | 9 | // Middlewares 10 | app.use(bodyParser.json({ limit: "50mb" })); 11 | app.use(bodyParser.urlencoded({ limit: "50mb", extended: true })); 12 | app.use(cors()); 13 | 14 | // Stock API routes 15 | const stockRoutes = require("./stock"); 16 | app.use("/api/stocks", stockRoutes); 17 | 18 | // Option API routes 19 | const optionsRoutes = require("./options"); 20 | app.use("/api/options", optionsRoutes); 21 | 22 | // Setup server 23 | const port = 8000; 24 | app.listen(port); 25 | console.log(`Server listening on port ${port}`); 26 | -------------------------------------------------------------------------------- /src/components/skeletons/quoteSkeleton.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Skeleton from "@material-ui/lab/Skeleton"; 3 | import { 4 | Typography, 5 | Card, 6 | CardContent, 7 | Grid 8 | } from "@material-ui/core"; 9 | 10 | const QuoteSkeleton = () => { 11 | return ( 12 | 13 | 14 | 15 | 16 | 17 | Use the search bar on the top to lookup an underlying! 18 | {/* */} 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | ); 32 | }; 33 | export default QuoteSkeleton; -------------------------------------------------------------------------------- /src/components/chain/ivSkewChart.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | Line, 4 | LineChart, 5 | Legend, 6 | XAxis, 7 | YAxis, 8 | CartesianGrid, 9 | Tooltip, 10 | Label, 11 | } from "recharts"; 12 | 13 | const IVSkewChart = (props) => { 14 | return( 15 | 21 | 22 | 23 | 25 | 26 | 27 | 28 | 29 | 30 | ) 31 | } 32 | 33 | export default IVSkewChart; -------------------------------------------------------------------------------- /src/components/chain/skeletons/selectSkeleton.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Skeleton from "@material-ui/lab/Skeleton"; 3 | import { 4 | Typography, 5 | Grid 6 | } from "@material-ui/core"; 7 | 8 | const SelectSkeleton = () => { 9 | return ( 10 | <> 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | ); 33 | }; 34 | export default SelectSkeleton; -------------------------------------------------------------------------------- /src/components/chain/skeletons/accordionSkeleton.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Skeleton from "@material-ui/lab/Skeleton"; 3 | import { 4 | Typography, 5 | Grid, 6 | Card, 7 | CardContent, 8 | } from "@material-ui/core"; 9 | 10 | const AccordionSkeleton = () => { 11 | return ( 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | ); 34 | }; 35 | export default AccordionSkeleton; -------------------------------------------------------------------------------- /src/utils/date.js: -------------------------------------------------------------------------------- 1 | // const parseDate = (str) => { 2 | // var mdy = str.split('-'); 3 | // return new Date(mdy[2], mdy[0]-1, mdy[1]); 4 | // } 5 | 6 | export const daysTillExpiry = (date1, date2) => { 7 | const firstDate = new Date(date1); 8 | const secondDate = new Date(date2); 9 | const msPerDay = 1000 * 60 * 60 * 24; 10 | const diff = (secondDate - firstDate)/msPerDay; 11 | return diff < 0 ? 0 : diff; 12 | } 13 | 14 | export const getCurrentDate = () => { 15 | var today = new Date(); 16 | var dd = String(today.getDate()).padStart(2, '0'); 17 | var mm = String(today.getMonth() + 1).padStart(2, '0'); //January is 0! 18 | var yyyy = today.getFullYear(); 19 | today = mm + '-' + dd + '-' + yyyy; 20 | return today; 21 | } 22 | 23 | export const getMaxDate = (dates) => { 24 | return new Date(Math.max.apply(null, dates.map(date => { 25 | return new Date(date); 26 | }))); 27 | } 28 | -------------------------------------------------------------------------------- /src/components/navTabs.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { makeStyles } from '@material-ui/core/styles'; 3 | import Paper from '@material-ui/core/Paper'; 4 | import Tabs from '@material-ui/core/Tabs'; 5 | import Tab from '@material-ui/core/Tab'; 6 | 7 | const useStyles = makeStyles({ 8 | root: { 9 | flexGrow: 1, 10 | }, 11 | }); 12 | 13 | const NavTabs = props => { 14 | const classes = useStyles(); 15 | const [value, setValue] = React.useState(0); 16 | 17 | const handleChange = (event, newValue) => { 18 | props.onChangeTabs(newValue); 19 | setValue(newValue); 20 | }; 21 | 22 | return ( 23 | 24 | 31 | 32 | 33 | 34 | 35 | ); 36 | } 37 | 38 | export default NavTabs; -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap'); 2 | .App { 3 | margin: 0; 4 | /* font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 5 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 6 | sans-serif; */ 7 | background-color: rgba(255, 255, 255, 0.264); 8 | font-family: 'Roboto', sans-serif; 9 | -webkit-font-smoothing: antialiased; 10 | -moz-osx-font-smoothing: grayscale; 11 | } 12 | 13 | code { 14 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 15 | monospace; 16 | } 17 | 18 | html { 19 | overflow-y: none; 20 | background-color: rgb(246, 246, 246); 21 | } 22 | body { 23 | overflow-y:scroll; 24 | } 25 | 26 | ::-webkit-scrollbar { 27 | -webkit-appearance: none; 28 | width: 7px; 29 | } 30 | 31 | ::-webkit-scrollbar-thumb { 32 | border-radius: 4px; 33 | background-color: rgba(0, 0, 0, .5); 34 | box-shadow: 0 0 1px rgba(255, 255, 255, .5); 35 | } -------------------------------------------------------------------------------- /src/components/chain/greeksChart.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | Line, 4 | LineChart, 5 | Legend, 6 | XAxis, 7 | YAxis, 8 | CartesianGrid, 9 | Tooltip, 10 | Label, 11 | } from "recharts"; 12 | 13 | const GreeksChart = (props) => { 14 | return( 15 | 21 | 22 | 23 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | ) 35 | } 36 | 37 | export default GreeksChart; -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Rahul Joshi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /server/options.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const router = express.Router(); 3 | const request = require("request"); 4 | const { API_KEY, API_BASE_URL } = require('./config'); 5 | 6 | router.post("/expiries", async(req,res) => { 7 | const symbol = req.body.symbol; 8 | 9 | request({ 10 | method: 'get', 11 | url: `${API_BASE_URL}markets/options/expirations`, 12 | qs: { 13 | symbol: symbol, 14 | includeAllRoots: 'true', 15 | strikes: 'false' 16 | }, 17 | headers: { 18 | Authorization: `Bearer ${API_KEY}`, 19 | Accept: 'application/json' 20 | } 21 | }, (error, response, body) => { 22 | res.send(body) 23 | }); 24 | }) 25 | 26 | router.post("/getChain", async(req, res) => { 27 | const symbol = req.body.symbol; 28 | const expiry = req.body.expiry; 29 | 30 | request({ 31 | method: 'get', 32 | url: `${API_BASE_URL}markets/options/chains`, 33 | qs: { 34 | symbol: symbol, 35 | expiration: expiry, 36 | greeks: 'true' 37 | }, 38 | headers: { 39 | Authorization: `Bearer ${API_KEY}`, 40 | Accept: 'application/json' 41 | } 42 | }, (error, response, body) => { 43 | res.send(body); 44 | }); 45 | }) 46 | 47 | module.exports = router; -------------------------------------------------------------------------------- /src/components/chain/expiries.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { 3 | TextField, 4 | } from '@material-ui/core'; 5 | import { Autocomplete } from '@material-ui/lab'; 6 | import axios from 'axios'; 7 | import { SERVER_URL } from '../../utils/url'; 8 | 9 | class Expiries extends Component { 10 | constructor(props) { 11 | super(props); 12 | this.state = { 13 | expirations: [], 14 | }; 15 | } 16 | 17 | componentDidUpdate(prevProps, prevState) { 18 | if(this.props.symbol !== prevProps.symbol) { 19 | this.getOptionExpiries(this.props.symbol); 20 | } 21 | } 22 | 23 | getOptionExpiries = symbol => { 24 | if(symbol) { 25 | axios 26 | .post(`${SERVER_URL}/api/options/expiries`, { 27 | symbol: symbol 28 | }) 29 | .then((res) => { 30 | const expirations = res.data.expirations.date; 31 | this.setState({ expirations }); 32 | }) 33 | .catch((err) => { 34 | console.log(err) 35 | }) 36 | } 37 | }; 38 | 39 | // triggers every time a user selects an option from suggestions 40 | valueChange = (event, value) => { 41 | if (!(value === null)) { 42 | console.log(value); 43 | this.props.onExpiryChange(value); 44 | } 45 | }; 46 | 47 | render() { 48 | return( 49 | option} 53 | onChange={this.valueChange} 54 | renderInput={(params) => } 55 | /> 56 | ) 57 | } 58 | } 59 | 60 | export default Expiries; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "options-tool", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@material-ui/core": "^4.11.2", 7 | "@material-ui/icons": "^4.11.2", 8 | "@material-ui/lab": "^4.0.0-alpha.57", 9 | "@testing-library/jest-dom": "^5.11.8", 10 | "@testing-library/react": "^11.2.2", 11 | "@testing-library/user-event": "^12.6.0", 12 | "axios": "^0.21.1", 13 | "bcrypt": "^5.0.0", 14 | "black-scholes": "^1.1.0", 15 | "body-parser": "^1.19.0", 16 | "bootstrap": "^4.5.3", 17 | "chart.js": "^2.9.4", 18 | "clone": "^2.1.2", 19 | "concurrently": "^5.3.0", 20 | "cors": "^2.8.5", 21 | "dotenv": "^8.2.0", 22 | "express": "^4.17.1", 23 | "greeks": "^1.0.0", 24 | "implied-volatility": "^1.0.0", 25 | "js-polynomial-regression": "^0.9.1", 26 | "nodemon": "^2.0.6", 27 | "path": "^0.12.7", 28 | "react": "^17.0.1", 29 | "react-bootstrap": "^1.4.0", 30 | "react-chartjs-2": "^2.11.1", 31 | "react-dom": "^17.0.1", 32 | "react-scripts": "4.0.1", 33 | "react-toastify": "^7.0.3", 34 | "recharts": "^2.0.3", 35 | "regression": "^2.0.1", 36 | "web-vitals": "^0.2.4" 37 | }, 38 | "scripts": { 39 | "start": "concurrently -n client,server -c blue,red \"react-scripts start\" \"nodemon server/app.js\"", 40 | "build": "react-scripts build", 41 | "test": "react-scripts test", 42 | "eject": "react-scripts eject" 43 | }, 44 | "eslintConfig": { 45 | "extends": [ 46 | "react-app", 47 | "react-app/jest" 48 | ] 49 | }, 50 | "browserslist": { 51 | "production": [ 52 | ">0.2%", 53 | "not dead", 54 | "not op_mini all" 55 | ], 56 | "development": [ 57 | "last 1 chrome version", 58 | "last 1 firefox version", 59 | "last 1 safari version" 60 | ] 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | GraphVega 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/components/quote.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | Row, 4 | Col 5 | } from "react-bootstrap"; 6 | import { 7 | TextField, 8 | IconButton, 9 | Card, 10 | CardContent, 11 | } from "@material-ui/core"; 12 | import AddIcon from '@material-ui/icons/Add'; 13 | import RemoveIcon from '@material-ui/icons/Remove'; 14 | 15 | // React function to display stock quote. 16 | const Quote = props => { 17 | 18 | const setColor = () => { 19 | return props.quote.change >= 0 ? 'text-success' : 'text-danger'; 20 | }; 21 | 22 | const setSign = () => { 23 | return props.quote.change >= 0 ? '+':''; 24 | } 25 | 26 | return( 27 | 28 | 29 | 30 | 31 |

32 | {props.quote.description} 33 |

34 |   35 |
({props.quote.symbol})
36 |   37 |

{props.quote.last}

38 |    39 |
40 | {setSign()}{props.quote.change} 41 |
42 |    43 |
44 | ({setSign()}{props.quote.change_percentage}%) 45 |
46 | 47 | 48 | 49 | 50 | 51 | 59 | 60 | 61 | 62 | 63 |
64 |
65 |
66 | ) 67 | 68 | 69 | } 70 | 71 | export default Quote; -------------------------------------------------------------------------------- /src/components/analysis/lineChart.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | AreaChart, 4 | Area, 5 | XAxis, 6 | YAxis, 7 | CartesianGrid, 8 | Tooltip, 9 | Label, 10 | } from "recharts"; 11 | import { 12 | Card, 13 | CardContent, 14 | Typography 15 | } from '@material-ui/core'; 16 | import { 17 | Row, 18 | Col 19 | } from "react-bootstrap"; 20 | 21 | 22 | const LineChart = (props) => { 23 | 24 | const data = props.data || []; 25 | 26 | const gradientOffset = () => { 27 | const dataMax = Math.max(...data.map((i) => i.profit)); 28 | const dataMin = Math.min(...data.map((i) => i.profit)); 29 | 30 | if (dataMax <= 0){ 31 | return 0 32 | } 33 | else if (dataMin >= 0){ 34 | return 1 35 | } 36 | else{ 37 | return dataMax / (dataMax - dataMin); 38 | } 39 | } 40 | 41 | const off = gradientOffset(); 42 | 43 | return( 44 | 45 | 46 | 47 | 48 | 49 | Profit & Loss Chart 50 | 51 | 52 | 53 | 54 | 55 | 56 | 62 | 63 | 64 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | ) 81 | } 82 | 83 | export default LineChart; -------------------------------------------------------------------------------- /src/components/chain/positions.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { 3 | Modal, 4 | Table, 5 | } from "react-bootstrap"; 6 | import { 7 | Button, 8 | Badge, 9 | } from '@material-ui/core'; 10 | import ClearIcon from '@material-ui/icons/Clear'; 11 | 12 | const Positions = props => { 13 | const [show, setShow] = useState(false); 14 | 15 | const handleClose = () => setShow(false); 16 | const handleShow = () => setShow(true); 17 | 18 | const displayQuantity = index => { 19 | return props.positions[index].position === "long" ? ( 20 |
+{props.positions[index].quantity}
21 | ) : ( 22 |
{props.positions[index].quantity}
23 | ); 24 | }; 25 | return ( 26 | <> 27 | 28 | 31 | 32 | 33 | 38 | 39 |

40 | Positions 41 |

42 |
43 | {props.positions[0]? 44 | "" 45 | :
Looks like you have not added any positions yet!
46 | } 47 | 48 | {props.positions.map((option, index) => { 49 | return ( 50 | 51 | 54 | 57 | 69 | 70 | ); 71 | })} 72 |
52 | {displayQuantity(index)} 53 | 55 | {option["description"]} 56 | 58 | 68 |
73 |
74 | 75 | 78 | 79 |
80 | 81 | ); 82 | }; 83 | 84 | export default Positions; 85 | -------------------------------------------------------------------------------- /server/stock.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const router = express.Router(); 3 | const request = require("request"); 4 | const { API_KEY, API_BASE_URL } = require("./config"); 5 | 6 | // Lookup based on company name 7 | router.post("/search", async (req, res) => { 8 | request( 9 | { 10 | method: "get", 11 | url: `${API_BASE_URL}markets/search`, 12 | qs: { 13 | q: req.body.ticker, 14 | exchanges: "Q,N", 15 | types: "stock", 16 | }, 17 | headers: { 18 | Authorization: `Bearer ${API_KEY}`, 19 | Accept: "application/json", 20 | }, 21 | }, 22 | (error, response, body) => { 23 | // the api key error is different from the vanilla error 24 | try { 25 | if (error) { 26 | throw new Error("Something went wrong! Please try again later."); 27 | } 28 | if (body == "Invalid Access Token") { 29 | throw new Error("Invalid Access Token"); 30 | } 31 | // console.log(body) 32 | res.send(body); 33 | } catch (err) { 34 | console.log(err) 35 | return res.status(500).json({ error: err.toString() }); 36 | } 37 | } 38 | ); 39 | }); 40 | 41 | // Lookup based on symbol 42 | router.post("/lookup", async (req, res) => { 43 | request( 44 | { 45 | method: "get", 46 | url: `${API_BASE_URL}markets/lookup`, 47 | qs: { 48 | q: req.body.ticker, 49 | exchanges: "Q,N", 50 | types: "stock", 51 | }, 52 | headers: { 53 | Authorization: `Bearer ${API_KEY}`, 54 | Accept: "application/json", 55 | }, 56 | }, 57 | (error, body) => { 58 | // the api key error is different from the vanilla error 59 | try { 60 | if (error) { 61 | throw new Error("Something went wrong! Please try again later."); 62 | } 63 | if (body == "Invalid Access Token") { 64 | throw new Error("Invalid Access Token"); 65 | } 66 | // console.log(body) 67 | res.send(body); 68 | } catch (err) { 69 | console.log(err) 70 | return res.status(500).json({ error: err.toString() }); 71 | } 72 | } 73 | ); 74 | }); 75 | 76 | // get quote of a company using symbol. 77 | router.post("/quote", async (req, res) => { 78 | const symbol = req.body.ticker; 79 | request( 80 | { 81 | method: "get", 82 | url: `${API_BASE_URL}markets/quotes`, 83 | qs: { 84 | symbols: symbol, 85 | greeks: "false", 86 | }, 87 | headers: { 88 | Authorization: `Bearer ${API_KEY}`, 89 | Accept: "application/json", 90 | }, 91 | }, 92 | (error, response, body) => { 93 | // console.log(body); 94 | console.log(error) 95 | 96 | res.send(body); 97 | } 98 | ); 99 | }); 100 | 101 | module.exports = router; 102 | -------------------------------------------------------------------------------- /src/components/chain/editLayout.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import EditLayoutModal from './editLayoutModal'; 3 | 4 | class EditLayout extends Component { 5 | constructor(props) { 6 | super(props); 7 | this.state = { 8 | layoutOptions: [ 9 | "change", 10 | "change_percentage", 11 | "open", 12 | "high", 13 | "volume", 14 | "prevclose", 15 | "open_interest", 16 | "average_volume" 17 | ], 18 | layout: ["bid", "ask", "last"], 19 | tempLayout: [], 20 | tempLayoutOptions: [], 21 | }; 22 | } 23 | componentDidMount() { 24 | const tempLayout = [...this.state.layout]; 25 | const tempLayoutOptions = [...this.state.layoutOptions]; 26 | this.setState({tempLayout, tempLayoutOptions}); 27 | } 28 | 29 | handleAddLayoutItem = i => { 30 | const tempLayout = [...this.state.tempLayout]; 31 | const tempLayoutOptions = [...this.state.tempLayoutOptions]; 32 | tempLayout.push(this.state.tempLayoutOptions[i]); 33 | tempLayoutOptions.splice(i, 1); 34 | this.setState({ tempLayoutOptions, tempLayout }); 35 | }; 36 | 37 | handleRemoveLayoutItem = i => { 38 | const tempLayout = [...this.state.tempLayout]; 39 | const tempLayoutOptions = [...this.state.tempLayoutOptions]; 40 | tempLayoutOptions.push(this.state.tempLayout[i]); 41 | tempLayout.splice(i, 1); // removes item 42 | this.setState({ tempLayoutOptions, tempLayout }); 43 | }; 44 | 45 | handleMoveUp = i => { 46 | if (i > 0) { 47 | const tempLayout = [...this.state.tempLayout]; 48 | const temp = tempLayout[i - 1]; 49 | tempLayout[i - 1] = tempLayout[i]; 50 | tempLayout[i] = temp; 51 | this.setState({ tempLayout }); 52 | } 53 | }; 54 | 55 | handleMoveDown = i => { 56 | if (i < this.state.tempLayout.length - 1) { 57 | const tempLayout = [...this.state.tempLayout]; 58 | const temp = tempLayout[i + 1]; 59 | tempLayout[i + 1] = tempLayout[i]; 60 | tempLayout[i] = temp; 61 | this.setState({ tempLayout }); 62 | } 63 | }; 64 | 65 | handleSaveLayout = () => { 66 | const layout = [...this.state.tempLayout]; 67 | const layoutOptions = [...this.state.tempLayoutOptions]; 68 | this.setState({ layout, layoutOptions }); 69 | this.props.onLayoutChange(layout); 70 | }; 71 | 72 | handleClose = () => { 73 | const tempLayout = [...this.state.layout]; 74 | const tempLayoutOptions = [...this.state.layoutOptions]; 75 | this.setState({ tempLayout, tempLayoutOptions }); 76 | }; 77 | 78 | render() { 79 | return ( 80 | 90 | ) 91 | } 92 | } 93 | 94 | export default EditLayout; -------------------------------------------------------------------------------- /src/components/chain/optionTable.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Row from "react-bootstrap/Row"; 3 | import Col from "react-bootstrap/Col"; 4 | import Table from "react-bootstrap/Table"; 5 | import Option from "./option"; 6 | 7 | const OptionTable = props => { 8 | return ( 9 | 10 | 11 | 12 | 13 |
Calls
14 | 15 | 16 | {props.expiry} 17 | 18 | 19 |
Puts
20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 | {props.layout.map((item, idx) => { 29 | return ; 30 | })} 31 | 32 | 33 | 34 | {props.calls.map((option, index) => ( 35 | 44 |
{item}
45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | {props.calls.map(option => ( 55 | 56 | 57 | 58 | ))} 59 | 60 |
Strike
{option.strike}
61 | 62 | 63 | 64 | 65 | 66 | {props.layout.map((item, idx) => { 67 | return ; 68 | })} 69 | 70 | 71 | 72 | {props.puts.map((option, index) => ( 73 | 82 |
{item}
83 | 84 |
85 | 86 |
87 | ); 88 | }; 89 | 90 | export default OptionTable; 91 | -------------------------------------------------------------------------------- /src/components/analysis/positions.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | Row, 4 | Col 5 | } from "react-bootstrap"; 6 | import { 7 | Card, 8 | CardContent, 9 | Table, 10 | TableHead, 11 | TableBody, 12 | TableRow, 13 | TableCell, 14 | Chip, 15 | Typography 16 | } from '@material-ui/core'; 17 | 18 | const chipStyle = type => { 19 | return type==="call"?"primary":"secondary"; 20 | } 21 | 22 | const expiry = date => { 23 | var d = new Date(date); 24 | var dd = String(d.getDate()).padStart(2, '0'); 25 | var mm = String(d.getMonth() + 1).padStart(2, '0'); //January is 0! 26 | var yyyy = d.getFullYear(); 27 | const today = mm + '-' + dd + '-' + yyyy; 28 | return today; 29 | } 30 | const roundOne = (num) => { 31 | return Math.round((num + Number.EPSILON) * 10)/10; 32 | } 33 | const Positions = props => { 34 | return ( 35 | 36 | 37 | 38 | 39 | 40 | Positions 41 | 42 | 43 | 44 | 45 | TYPE 46 | QTY 47 | MARK 48 | STRIKE 49 | EXPIRY 50 | IMP VOL 51 | VEGA 52 | THETA 53 | DELTA 54 | GAMMA 55 | 56 | 57 | 58 | {props.positions.map((option) => ( 59 | 60 | 61 | 62 | 63 | {option.quantity} 64 | {(option.bid + option.ask)/2} 65 | {option.strike} 66 | {expiry(option.expiration_date)} 67 | {roundOne(option.greeks.smv_vol*100)}% 68 | {option.greeks.vega} 69 | {option.greeks.theta} 70 | {option.greeks.delta} 71 | {option.greeks.gamma} 72 | 73 | ))} 74 | {(props.quantity && props.quantity !== 0) ? 75 | 76 | 77 | 78 | 79 | {props.quantity} 80 | {(props.quote.ask + props.quote.bid)/2} 81 | -- 82 | -- 83 | -- 84 | -- 85 | -- 86 | {props.quantity} 87 | -- 88 | 89 | : "" 90 | } 91 | 92 |
93 |
94 |
95 | 96 |
97 | ) 98 | } 99 | 100 | export default Positions; -------------------------------------------------------------------------------- /src/components/search.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { TextField, CircularProgress } from "@material-ui/core"; 3 | import { Autocomplete } from "@material-ui/lab"; 4 | import { toast } from "react-toastify"; 5 | import axios from 'axios'; 6 | import { SERVER_URL } from '../utils/url'; 7 | 8 | class Search extends Component { 9 | constructor(props) { 10 | super(props); 11 | this.state = { 12 | options: [], 13 | value: {}, 14 | loading: false, 15 | }; 16 | } 17 | 18 | // triggers every time a user changes the input value 19 | handleInputValueChange = async (event, value) => { 20 | var url = `${SERVER_URL}/api/stocks/`; 21 | var regExp = /\([^)]*\)/g; 22 | var matchTest = value.match(regExp); 23 | if (!!matchTest) { 24 | url += "lookup"; 25 | value = value.replace(/[()]/g, ""); //removing parentheses 26 | } else { 27 | url += "search"; //default to search 28 | } 29 | console.log(url); 30 | try { 31 | this.setState({ loading: true }, () => { 32 | axios 33 | .post(url, { 34 | ticker: value, 35 | }) 36 | .then((res) => { 37 | var options; 38 | if (res.data.securities && res.data.securities.security) { 39 | if (res.data.securities.security.length) { 40 | options = res.data.securities.security; 41 | } else { 42 | const resSecurity = JSON.parse(JSON.stringify(res.data.securities.security)); 43 | const emptySecurity = JSON.parse('{"symbol":"","exchange":"","type":"","description":""}'); 44 | const data = Object.assign({}, res.data); 45 | data.securities.security = []; 46 | data.securities.security.push(resSecurity); 47 | data.securities.security.push(emptySecurity); 48 | options = data.securities.security; 49 | } 50 | const upper = options.length > 10? 10: options.length > 1? options.length - 1: 1; 51 | options = options.slice(0, upper); 52 | this.setState({ options, loading: false }); 53 | } else { 54 | this.setState({ options: [], loading: false }); 55 | } 56 | }); 57 | }) 58 | } catch (err) { 59 | toast.error(err.response?.data?.error || "Something went wrong! Please try again later."); 60 | } finally { 61 | this.setState({ loading: false }); 62 | } 63 | }; 64 | 65 | // triggers every time a user selects an option from suggestions 66 | valueChange = (event, value) => { 67 | if (value !== null) { 68 | this.props.onValueChange(value); 69 | } 70 | }; 71 | 72 | render() { 73 | return ( 74 | ( 78 | <> 79 | {option.description}  80 |
({option.symbol})
81 | 82 | )} 83 | getOptionLabel={(option) => option.description} 84 | getOptionSelected={(option, value) => 85 | option.description === value.description 86 | } 87 | onChange={this.valueChange} 88 | onInputChange={this.handleInputValueChange} 89 | filterOptions={(x) => x} 90 | renderInput={(params) => ( 91 | 99 | {this.state.loading ? ( 100 | 101 | ) : null} 102 | {params.InputProps.endAdornment} 103 | 104 | ), 105 | }} 106 | /> 107 | )} 108 | /> 109 | ); 110 | } 111 | } 112 | 113 | export default Search; 114 | -------------------------------------------------------------------------------- /src/components/chain/editLayoutModal.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { 3 | Modal, 4 | Row, 5 | Col, 6 | Table, 7 | ButtonGroup, 8 | } from 'react-bootstrap'; 9 | import { 10 | Button 11 | } from '@material-ui/core'; 12 | import { 13 | Add, 14 | Remove, 15 | ArrowUpward, 16 | ArrowDownward, 17 | } from '@material-ui/icons'; 18 | 19 | const EditLayoutModal = props => { 20 | const [show, setShow] = useState(false); 21 | const handleClose = () => { 22 | setShow(false); 23 | setTimeout(() => { 24 | props.onClose(); 25 | }, 500); 26 | }; 27 | const handleShow = () => setShow(true); 28 | 29 | return ( 30 | <> 31 | 34 | 35 | 41 | 42 | Edit Layout 43 | 44 | 45 | 46 | 47 |
48 |
Layout options
49 |
50 | 51 | 52 | {props.layoutOptions.map((item, index) => { 53 | return ( 54 | 55 | 56 | 68 | 69 | ); 70 | })} 71 | 72 |
{item} 57 | 67 |
73 | 74 | 75 |
76 |
Current Layout
77 |
78 | 79 | 80 | {props.layout.map((item, index) => { 81 | return ( 82 | 83 | 84 | 115 | 116 | ); 117 | })} 118 | 119 |
{item} 85 | 95 |   96 | 97 | 105 | 113 | 114 |
120 | 121 |
122 |
123 | 124 | 127 |   128 | 138 | 139 |
140 | 141 | ); 142 | }; 143 | 144 | export default EditLayoutModal; -------------------------------------------------------------------------------- /src/utils/bsm.js: -------------------------------------------------------------------------------- 1 | import clone from 'clone'; 2 | import bs from "black-scholes"; 3 | import iv from "implied-volatility"; 4 | import {daysTillExpiry, getCurrentDate} from './date'; 5 | 6 | // Set end point of the chart as 30% above the stock price 7 | const getHigh = (price, high) => { 8 | return price + price/3; 9 | } 10 | 11 | // Set end point of the chart as 30% above the stock price 12 | const getLow = (price, low) => { 13 | return price - price/3; 14 | } 15 | 16 | // Round a number to 2 decimal points 17 | const round = (num) => { 18 | return Math.round((num + Number.EPSILON) * 100)/100; 19 | } 20 | 21 | // Round a number to 1 decimal point 22 | const roundOne = (num) => { 23 | return Math.round((num + Number.EPSILON) * 10)/10; 24 | } 25 | 26 | /** Main function used to obtain the data used in the chart. 27 | * @param {Array} positions - positions as stored in main.jsx 28 | * @param {Object} quote - quote object of stock as stored in main.jsx 29 | * @param {Integer} quantity - number of underlying shares (stored in main.jsx) 30 | * @param {String} date - date as determined by slider in analysis.jsx 31 | * @param {Float} ivChange - difference b/w average IV and new IV (slider) divided 32 | * by the number of positions. 33 | * @return {Array} netProfit - Array consisting of objects {underlying, price, profit} 34 | */ 35 | export const netProfitArray = (positions, quote, quantity, date, ivChange) => { 36 | // calculate P/L for options positions 37 | var netProfit = clone(optionPriceArray(positions[0], quote, date, ivChange)); 38 | var i = 1; 39 | for(i=1; i ( 50 | { 51 | underlying: obj.underlying, 52 | price: round(obj.price*100 + quantity * obj.underlying), 53 | profit: round(obj.profit*100 + quantity * (obj.underlying - quote.last)) 54 | } 55 | )) 56 | } 57 | return netProfit; 58 | } 59 | 60 | /** 61 | * @param {Array} positions - positions as stored in main.jsx' state. 62 | * @param {Object} quote - quote as stored in main.jsx' state. 63 | * @returns - average volatility of positions (number) 64 | */ 65 | export const avgVolatility = (positions, quote) => { 66 | var vol = 0; 67 | positions.forEach(option => { 68 | const optionPrice = (option.ask + option.bid)/2; 69 | const daysDiff = daysTillExpiry(getCurrentDate(), option.expiration_date); 70 | vol += iv.getImpliedVolatility( 71 | optionPrice, quote.last, option.strike, daysDiff/365, 0.05, option.option_type 72 | ); 73 | console.log(vol); 74 | }); 75 | return round(vol/positions.length); 76 | } 77 | 78 | /** Function used to obtain the P/L data for a single option. 79 | * @param {Object} option - object storing data of the option, element of positions. 80 | * @param {Object} quote - quote object of stock as stored in main.jsx 81 | * @param {String} date - date as determined by slider in analysis.jsx 82 | * @param {Float} volChange- difference b/w average IV and new IV (slider) divided 83 | * by the number of positions. 84 | * @return {Array} priceArray - Array consisting of objects {underlying, price, profit} 85 | */ 86 | const optionPriceArray = (option, quote, date, volChange) => { 87 | var priceArray = [] 88 | var optionPrice = (option.ask + option.bid)/2; // option price 89 | var price = 0; // underlying price 90 | 91 | // setting constants 92 | const quantity = Math.abs(option.quantity); 93 | const high = getHigh(quote.last, quote.week_52_high); 94 | const low = getLow(quote.last, quote.week_52_low); 95 | const initial = (option.ask + option.bid)/2; 96 | const mul = option.position === "short" ? -1 : 1; 97 | const interval = quote.last * 0.2/100; 98 | 99 | // calculate default days diff 100 | var daysDiff = daysTillExpiry(new Date().toString(), option.expiration_date); 101 | // find volatility of the options as of today (current date) 102 | var vol = iv.getImpliedVolatility( 103 | optionPrice, quote.last, option.strike, daysDiff/365, 0.05, option.option_type 104 | ) + volChange/100; 105 | // find new daysDiff if date is adjusted by slider. 106 | daysDiff = daysTillExpiry(date, option.expiration_date); 107 | price = low; 108 | // create priceArray 109 | while(price < high) { 110 | optionPrice = bs.blackScholes( 111 | price, 112 | option.strike, 113 | daysDiff/365, 114 | vol, 115 | 0.05, 116 | option.option_type 117 | ); 118 | priceArray.push({ 119 | underlying: roundOne(price), 120 | price: mul * round(optionPrice * quantity), 121 | profit: mul * round((optionPrice - initial) * quantity), 122 | }); 123 | price += interval; 124 | } 125 | return priceArray; 126 | } 127 | -------------------------------------------------------------------------------- /src/components/analysis/analysis.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component } from 'react'; 2 | import { 3 | Row, 4 | Col, 5 | } from 'react-bootstrap'; 6 | import { 7 | Slider, 8 | Card, 9 | CardContent, 10 | Typography, 11 | } from '@material-ui/core'; 12 | import { 13 | avgVolatility, 14 | netProfitArray, 15 | } from "../../utils/bsm"; 16 | import { 17 | daysTillExpiry, 18 | getCurrentDate, 19 | getMaxDate, 20 | } from "../../utils/date"; 21 | import LineChart from './lineChart'; 22 | import Positions from './positions'; 23 | 24 | // Component that handles analysis of options positions 25 | class Analysis extends Component { 26 | constructor(props) { 27 | super(props); 28 | this.state = { 29 | dateNum: 0, 30 | maxDateNum: 0, 31 | date: "", 32 | originalIV: 0, 33 | iv: 0, 34 | ivChange: 0, 35 | chartData: [], 36 | } 37 | } 38 | 39 | componentDidUpdate(prevProps, prevState) { 40 | if(this.props !== prevProps) { 41 | console.log("Calculating...") 42 | const originalIV = this.roundOne(avgVolatility(this.props.positions, this.props.quote) * 100); 43 | var date = new Date(); 44 | date.setHours(21,0,0,0); 45 | date = date.toString(); 46 | const prices = netProfitArray( 47 | this.props.positions, 48 | this.props.quote, 49 | this.props.quantity, 50 | date, 51 | 0 52 | ); 53 | const maxDate = getMaxDate(this.props.positions.map(option => option.expiration_date)); 54 | const maxDateNum = Math.ceil(daysTillExpiry(date, maxDate)); 55 | var chartData = []; 56 | prices.forEach(obj => { 57 | chartData.push({ 58 | label: obj.underlying, 59 | profit: obj.profit, 60 | }) 61 | }) 62 | date = getCurrentDate(); 63 | this.setState({ chartData, originalIV, iv:originalIV, maxDateNum, date, ivChange:0 }); 64 | } 65 | } 66 | roundOne = (num) => { 67 | return Math.round((num + Number.EPSILON) * 10)/10; 68 | } 69 | 70 | setChartData = (arr) => { 71 | var chartData = []; 72 | arr.forEach(obj => { 73 | chartData.push({ 74 | label: obj.underlying, 75 | profit: obj.profit, 76 | }) 77 | }) 78 | return chartData; 79 | } 80 | 81 | handleIVChange = (event, newIV) => { 82 | var date = new Date(); 83 | date.setDate(date.getDate() + this.state.dateNum); 84 | const ivChange = newIV - this.state.originalIV; 85 | const prices = netProfitArray( 86 | this.props.positions, 87 | this.props.quote, 88 | this.props.quantity, 89 | date.toString(), 90 | ivChange/this.props.positions.length 91 | ); 92 | const chartData = this.setChartData(prices); 93 | this.setState({ iv:newIV, ivChange, chartData }); 94 | } 95 | 96 | handleDateChange = (event, dateNum) => { 97 | var date = new Date(); 98 | date.setDate(date.getDate() + dateNum); 99 | const dateString = date.getMonth() + 1 100 | + '-' + date.getDate() + '-' + date.getFullYear(); 101 | const prices = netProfitArray( 102 | this.props.positions, 103 | this.props.quote, 104 | this.props.quantity, 105 | date.toString(), 106 | this.state.ivChange/this.props.positions.length 107 | ); 108 | const chartData = this.setChartData(prices); 109 | this.setState({dateNum, date:dateString, chartData}) 110 | } 111 | 112 | render() { 113 | return( 114 | <> 115 | 120 |
121 | 122 | 123 | 124 | 125 |
126 | Implied volatility: {this.state.iv}% (avg: {this.state.originalIV}%) 127 |
128 | 135 |
136 |
137 | 138 | 139 | 140 | 141 |
142 | Days till last option expiry: {this.state.maxDateNum - this.state.dateNum} ({this.state.date}) 143 |
144 | 150 |
151 |
152 | 153 |
154 |
155 | 156 | 157 | 158 | 159 | 160 | 161 | ) 162 | } 163 | } 164 | 165 | export default Analysis; -------------------------------------------------------------------------------- /src/components/main.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { 3 | Row, 4 | Col, 5 | Container, 6 | } from 'react-bootstrap'; 7 | import Search from './search'; 8 | import Quote from './quote'; 9 | import OptionChain from './chain/optionChain'; 10 | import Analysis from './analysis/analysis'; 11 | import NavTabs from './navTabs'; 12 | import QuoteSkeleton from './skeletons/quoteSkeleton'; 13 | import axios from 'axios'; 14 | import { SERVER_URL } from '../utils/url'; 15 | 16 | class Main extends Component { 17 | state = { 18 | quantity: 0, 19 | quote:{}, 20 | positions:[], 21 | tab: 0, //0 for chain, 1 for analysis page 22 | } 23 | /** 24 | * Triggered when new stock is selected from search bar. 25 | * @param {Object} value: Object returned from search bar 26 | */ 27 | handleTickerChange = value => { 28 | this.getQuote(value.symbol); 29 | this.setState({ quote:{}, positions:[], tab:0, quantity:0 }); 30 | } 31 | 32 | /** 33 | * Function used to get the quote of a stock. Called when new stock 34 | * is selected from search bar. 35 | * @param {String} symbol: ticker of the stock 36 | */ 37 | getQuote = (symbol) => { 38 | this.setState({ quoteLoading: true }, () => { 39 | const url = `${SERVER_URL}/api/stocks/quote`; 40 | console.log(url); 41 | axios 42 | .post(url, { 43 | ticker: symbol, 44 | }) 45 | .then((res) => { 46 | const quote = res.data.quotes.quote; 47 | this.setState({ quote, quoteLoading: false }); 48 | }); 49 | }); 50 | }; 51 | 52 | 53 | /** 54 | * Function to add an option to the positions[] object 55 | * @param {Object} option Options object to add to positions[] 56 | */ 57 | handleAddPosition = (option) => { 58 | var positions = [...this.state.positions]; 59 | var found = false; 60 | for(var i = 0; i < positions.length; i++) { 61 | if(positions[i].description === option.description){ 62 | positions[i].quantity = positions[i].quantity + option.quantity; 63 | found = true; 64 | break; 65 | } 66 | } 67 | if(!found){ 68 | positions.push(option); 69 | } 70 | this.setState({positions}); 71 | } 72 | 73 | /** 74 | * Function to remove an options from positions[] 75 | * @param {Integer} idx remove object at index 'idx' from positions[] 76 | */ 77 | handleRemovePosition= idx => { 78 | const positions = [...this.state.positions]; 79 | positions.splice(idx, 1); 80 | this.setState({ positions }); 81 | } 82 | 83 | /** 84 | * Function handles switching tabs between OptionChain and Analysis 85 | * @param {Integer} value: sets tab to value 86 | */ 87 | handleChangeTabs = value => { 88 | if(value !== this.state.tab) { 89 | this.setState({tab:value}); 90 | } 91 | } 92 | 93 | /** 94 | * Adds quantity to/from stock. 95 | * @param {Object} event: triggered by adding/removing stock 96 | */ 97 | handleStockQuantityChange = (event) => { 98 | var quantity = Number(event.target.value); 99 | this.setState({quantity}) 100 | } 101 | 102 | // Adds 1 stock (increments quantity by 1.) 103 | handleAddStock = () => { 104 | const quantity = this.state.quantity? this.state.quantity + 1 : 1; 105 | this.setState({quantity}); 106 | } 107 | 108 | // Removes 1 stock (decrements quantity by 1.) 109 | handleRemoveStock = () => { 110 | const quantity = this.state.quantity? this.state.quantity - 1 : -1; 111 | this.setState({quantity}); 112 | } 113 | 114 | // decides whether OptionChain component will be displayed or not 115 | display = () => { 116 | return this.state.tab === 0 ? "block" : "none"; 117 | } 118 | 119 | // decides whether Analysis component is displayed or not. 120 | display2 = () => { 121 | return this.state.tab === 1? "block" : "none"; 122 | } 123 | 124 | render() { 125 | return( 126 | <> 127 | 128 | 129 | 130 |

131 | GraphVega 132 |

133 | 134 | 135 | 136 | 137 |
138 |
139 | 140 | 141 | {this.state.quote.symbol ? 142 | : } 149 | 150 | 151 | 152 |
153 | 154 | 155 |
156 | 157 |
158 | 159 |
160 | 161 |
162 | 163 | 164 |
165 | 171 |
172 | {this.state.positions[0]? 173 |
174 | 179 |
180 | : "" 181 | } 182 | 183 |
184 |
185 | 186 | ) 187 | } 188 | } 189 | 190 | export default Main; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 | 5 |

6 | 7 | GraphVega 8 | 9 | 10 |

11 | 12 |

13 | 14 | ## :gem: About The Project 15 | 16 | GraphVega is an open sourced options analytics platform that analyses 17 | and projects the P/L of a multi-legged options position with variation 18 | of the stock price under changes in volatility and days till expiration, 19 | using a black-scholes simulation. 20 | 21 | It is designed with a goal to provide a simple and intuitive interface 22 | to analyze options, while also providing developers with the flexibility 23 | to add their own custom features. 24 | 25 |

26 | 27 | 28 | 29 |

30 | 31 | ## :rocket: Getting Started 32 | 33 | **Note**: You will need to have NodeJS installed on your machine to run this app. If you don't have it on your machine already, you can install it [here](https://nodejs.org/en/download/) for free. 34 | 35 | To get GraphVega up and running on your local machine, follow these steps: 36 | 37 | 1. Clone the repository 38 | 39 | ``` 40 | $ git clone https://github.com/rahuljoshi44/GraphVega.git 41 | ``` 42 | 43 | 2. Switch to the root directory of the project (main folder where all the files are stored) and install the 44 | dependencies. This process might take a couple minutes depending on your download speed, so please wait! 45 | ``` 46 | $ npm install 47 | ``` 48 | 49 | 3. Get a free API Key (for sandbox) from Tradier 50 | [here](https://developer.tradier.com/user/sign_up?_ga=2.9691381.1305307848.1613100396-1783872143.1609733953). 51 | This project uses Tradier’s market data API for options and stock 52 | prices. 53 | 54 | 4. In the root directory create a `.env` file and enter your API key and the API url as 55 | follows: 56 | ``` 57 | TRADIER_API_KEY=YOUR_API_KEY_HERE 58 | API_BASE_URL=https://sandbox.tradier.com/v1/ 59 | ``` 60 | Replace `YOUR_API_KEY_HERE` with the API key you obtained from step 3. 61 | NOTE: You can also change the variable `API_BASE_URL` to the brokerage API url if you'd like to use the brokerage API endpoint. 62 | 63 | 5. Run the application in either of two ways: Locally or via Docker (explained below): 64 | 65 | ### Locally 66 | In the root directory, run the following command: 67 | 68 | $ npm start 69 | 70 | Note that the front end react app runs on `http://localhost:3000` while 71 | the server runs on `http://localhost:8000` so make sure you don’t have 72 | anything running on those ports. If you want to run the server on a different port, change the port variable in `server/app.js`, and change the`SERVER_URL` variable in `src/utils/url.js` to the new server url. 73 | 74 | ### Docker 75 | 76 | Make sure to create the `.env` file from step 4 above before building the 77 | image, otherwise it won't be included. 78 | 79 | Building: 80 | 81 | $ docker build -t local/gv:latest -t local/gv:0.1.0 . 82 | 83 | Running: 84 | 85 | $ docker run -d --rm -p 3000:3000 --name graphvega local/gv 86 | 87 | Stopping: 88 | 89 | $ docker stop graphvega 90 | 91 | ### Docker-Compose 92 | 93 | Make sure to create the `.env` file from step 4 above before building the 94 | image, otherwise it won't be included. 95 | 96 | Running: 97 | 98 | $ docker-compose up 99 | 100 | or 101 | 102 | $ docker-compose run 103 | 104 | Stopping: 105 | 106 | $ docker-compose down 107 | 108 | ## :zap: Usage 109 | 110 |

111 | 112 | 113 | 114 |

115 | 116 | 1. After launching the app, type in the name of a company in the search 117 | bar and select the appropriate suggestion. 118 | 2. Select an expiration date for the options chain 119 | 3. After the option chain loads, add your options positions by clicking 120 | on the rows of the table. 121 | 4. Switch to the analysis tab. 122 | 5. Observe the P/L chart and adjust the implied volatility and days 123 | till expiry with the sliders as you like. 124 | 125 | ## :palm_tree: Code Structure 126 | 127 | Broadly, the project is divided into the front end and the back end. 128 | - All frontend files are stored in the `src` directory. 129 | - Backend files are stored in `server`. These are used primarily for making API calls for market data. 130 | 131 | There are three main front end components 132 | - `src/components/main.jsx` is the root component that uses 133 | `optionChain.jsx` and `analysis.jsx` 134 | - `src/components/chain/optionChain.jsx` is the base component for the 135 | ‘Option Chain’ tab 136 | - All files related to the option chain tab is stored under `src/components/chain` 137 | - `src/components/analysis/analysis.jsx` is the base component for the 138 | ‘Analysis’ tab 139 | - All files related to the analysis tab are stored under `src/components/analysis` 140 | 141 | ## :heart: Contributing 142 | 143 | Your contributions make the platform better and more useful to everyone! The contributions you make will be greatly appreciated. 144 | 145 | To do so: 146 | 1. Fork the project 147 | 2. Create a branch 148 | 3. Add your changes 149 | 4. Push to the branch 150 | 5. Open a Pull Request. 151 | 152 | ## :pencil2: Built With 153 | 154 | - [React.js](https://reactjs.org/) - Front end library 155 | - [Node.js](https://nodejs.org/en/) - Runtime environment for JS 156 | - [Express.js](https://expressjs.com/) - Web framework for NodeJS 157 | - [Material-UI](https://material-ui.com/) - Front end component 158 | library 159 | - [react-bootstrap](https://react-bootstrap.github.io/) - Front end 160 | component library 161 | - [recharts](https://recharts.org/en-US/) - Charting library 162 | 163 | ## :clipboard: License 164 | 165 | GraphVega is distributed under the **MIT** license. See `LICENSE.md` for 166 | more information. 167 | 168 | ## :mailbox_with_mail: Contact 169 | 170 | [Rahul Joshi](https://www.linkedin.com/in/rahuljoshi4/) - 171 | rjoshi9@umd.edu 172 | 173 | Feel free to contact me regarding any concerns about the app. 174 | 175 | ## :punch: Acknowledgements 176 | 177 | Thanks to [Tradier](https://tradier.com/) for the market data used on 178 | the platform. 179 | -------------------------------------------------------------------------------- /src/components/chain/option.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { 3 | Modal, 4 | Table, 5 | Row, 6 | Col 7 | } from "react-bootstrap"; 8 | import { 9 | Button, 10 | TextField, 11 | IconButton 12 | } from '@material-ui/core'; 13 | import { 14 | Add, 15 | Remove, 16 | 17 | } from '@material-ui/icons'; 18 | 19 | const Option = props => { 20 | const [show, setShow] = useState(false); 21 | const handleClose = () => { 22 | setShow(false); 23 | setTimeout(() => { 24 | }, 500); 25 | }; 26 | const handleShow = () => setShow(true); 27 | 28 | const [quantity, setQuantity] = useState(0); 29 | 30 | const add = () => { 31 | setShow(false); 32 | const position = quantity > 0 ? "long" : "short"; 33 | props.onAddPosition(props.index, props.option["option_type"], position, quantity); 34 | // const quantity = 0; 35 | setQuantity(0); 36 | }; 37 | 38 | const getColor = () => { 39 | return props.option.change >= 0 ? "text-success" : "text-danger"; 40 | }; 41 | 42 | const setSign = () => { 43 | return props.option.change > 0 ? "+" : ""; 44 | }; 45 | 46 | // const setBackground = () => { 47 | // if(props.option.option_type === "put"){ 48 | // return props.option.strike > props.quote.last ? "#E0EBFD":"white"; 49 | // } 50 | // else{ 51 | // return props.option.strike > props.quote.last ? "white": "#E0EBFD"; 52 | // } 53 | // }; 54 | 55 | const setBorder = idx => { 56 | if(props.option.option_type==="put" && idx === props.layout.length-1) { 57 | const style = {}; 58 | style.borderRight = props.option.strike > props.quote.last ? "6px solid #3f51b5":""; 59 | return style; 60 | } 61 | else if (props.option.option_type==="call" && idx === 0){ 62 | const style = {}; 63 | style.borderLeft = props.option.strike < props.quote.last ? "10px solid #3f51b5":""; 64 | return style; 65 | } 66 | }; 67 | 68 | const displayItem = data => { 69 | return data ? data : "N/A"; 70 | } 71 | 72 | const addOption = () => { 73 | setQuantity(quantity + 1); 74 | } 75 | const removeOption = () => { 76 | setQuantity(quantity - 1); 77 | } 78 | 79 | const changeQuantity = (event, value) => { 80 | var quantity = Number(event.target.value); 81 | setQuantity(quantity) 82 | } 83 | 84 | const setButtonColor = () => { 85 | if(quantity >= 0) 86 | return "primary"; 87 | else 88 | return "secondary"; 89 | } 90 | 91 | return ( 92 | <> 93 | 94 | {props.layout.map((item, index) => { 95 | return {displayItem(props.option[item.toString()])}; 96 | })} 97 | 98 | 99 | 105 | 106 | 107 | 108 | 112 |

113 | {props.option.description} 114 |

115 |
116 |

{props.option.last}

117 |    118 |
119 | {setSign()}{props.option.change} 120 |
121 |    122 |
123 | ({setSign()}{props.option.change_percentage}%) 124 |
125 | 126 |
127 |
128 | 129 | 130 | 131 | 132 | 133 | 134 | 137 | 138 | 139 | 140 | 143 | 144 | 145 | 146 | 149 | 150 | 151 | 152 | 155 | 156 | 157 | 158 | 161 | 162 | 163 |
Bid 135 | {props.option.bid} 136 |
Ask 141 | {props.option.ask} 142 |
Open 147 | {props.option.open} 148 |
Previous Close 153 | {props.option.prevclose} 154 |
Open Interest 159 | {props.option.open_interest} 160 |
164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 174 | 175 | 176 | 177 | 180 | 181 | 182 | 183 | 186 | 187 | 188 | 189 | 192 | 193 | 194 | 195 | 198 | 199 | 200 |
Implied Volatility 172 | {props.option.greeks.mid_iv} 173 |
Delta 178 | {props.option.greeks.delta} 179 |
Gamma 184 | {props.option.greeks.gamma} 185 |
Theta 190 | {props.option.greeks.theta} 191 |
Vega 196 | {props.option.greeks.vega} 197 |
201 | 202 |
203 |
204 | 205 | 206 | 207 | 208 | 217 | 218 | 219 | 220 | 229 |   230 | 236 | 237 |
238 | 239 | ); 240 | }; 241 | 242 | export default Option; 243 | -------------------------------------------------------------------------------- /src/components/chain/optionChain.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import { 3 | Row, 4 | Col, 5 | } from 'react-bootstrap'; 6 | import { 7 | LinearProgress, 8 | Card, 9 | CardContent, 10 | Accordion, 11 | AccordionSummary, 12 | AccordionDetails, 13 | Grid, 14 | Typography, 15 | Select, 16 | MenuItem, 17 | } from '@material-ui/core'; 18 | import ExpandMoreIcon from '@material-ui/icons/ExpandMoreOutlined'; 19 | import Expiries from './expiries'; 20 | import EditLayout from './editLayout'; 21 | import OptionTable from './optionTable'; 22 | import Positions from './positions'; 23 | import IVSkewChart from './ivSkewChart'; 24 | import GreeksChart from './greeksChart'; 25 | import axios from 'axios'; 26 | import clone from 'clone'; 27 | import { SERVER_URL } from '../../utils/url'; 28 | 29 | class OptionChain extends Component { 30 | constructor(props) { 31 | super(props); 32 | this.state = { 33 | symbol: "", 34 | expiry:"", 35 | greeksChart: "calls", 36 | calls: [], 37 | puts: [], 38 | ivSkewData: [], 39 | callGreeksData: [], 40 | putGreeksData: [], 41 | layout: ["bid", "ask", "last"], 42 | loading: false, 43 | displayTable: false, 44 | }; 45 | } 46 | 47 | handleExpiryChange = date => { 48 | this.getOptionChain(this.props.quote.symbol, date); 49 | } 50 | 51 | // Rounds number to 2 decimal points 52 | round = num => { 53 | return Math.round((num + Number.EPSILON) * 100)/100; 54 | } 55 | 56 | getOptionChain = (symbol, expiry) => { 57 | this.setState({ loading: true }, () => { 58 | const url = `${SERVER_URL}/api/options/getChain`; 59 | axios 60 | .post(url, { 61 | symbol: symbol, 62 | expiry: expiry 63 | }) 64 | .then(res => { 65 | const options = res.data.options.option; 66 | const puts = options.filter(option => { 67 | return option.option_type === "put"; 68 | }); 69 | const calls = options.filter(option => { 70 | return option.option_type === "call"; 71 | }); 72 | var ivSkewData = [], callGreeksData = [], putGreeksData=[]; 73 | 74 | calls.forEach((call, idx) => { 75 | ivSkewData.push({ 76 | strike: call.strike, 77 | iv: call.greeks.smv_vol, 78 | }); 79 | callGreeksData.push({ 80 | strike: call.strike, 81 | delta: this.round(call.greeks.delta), 82 | gamma: this.round(call.greeks.gamma), 83 | theta: this.round(call.greeks.theta), 84 | vega: this.round(call.greeks.vega), 85 | }); 86 | putGreeksData.push({ 87 | strike: call.strike, 88 | delta: this.round(puts[idx].greeks.delta), 89 | gamma: this.round(puts[idx].greeks.gamma), 90 | theta: this.round(puts[idx].greeks.theta), 91 | vega: this.round(puts[idx].greeks.vega), 92 | }) 93 | }) 94 | // console.log(greeksData); 95 | this.setState({ 96 | puts, 97 | calls, 98 | ivSkewData, 99 | callGreeksData, 100 | putGreeksData, 101 | loading:false, 102 | displayTable: true, 103 | expiry, 104 | symbol 105 | }); 106 | }); 107 | }); 108 | }; 109 | 110 | handleLayoutChange = layout => { 111 | this.setState({layout}); 112 | } 113 | 114 | handleAddPosition = (idx, optionType, positionType, quantity) => { 115 | var option = {}; 116 | 117 | // Find option 118 | if(optionType==="call"){ 119 | option = clone(this.state.calls[idx]); 120 | } 121 | else { 122 | option = clone(this.state.puts[idx]); 123 | } 124 | 125 | // Set position -> Long/Short 126 | option.position = positionType; 127 | option.quantity = quantity; 128 | 129 | // Set expiry time 130 | var dateTime = new Date(option.expiration_date); 131 | dateTime.setHours(21,0,0,0); 132 | dateTime.setDate(dateTime.getDate() + 1); 133 | option.expiration_date = dateTime.toString(); 134 | 135 | this.props.onAddPosition(option); 136 | } 137 | 138 | handleGreeksChartChange = (event) => { 139 | const greeksChart = event.target.value; 140 | this.setState({ greeksChart }) 141 | } 142 | 143 | accChange = () => { 144 | const displayTable = !this.state.displayTable; 145 | this.setState({ displayTable }) 146 | } 147 | 148 | errorMessage = () => { 149 | return ( 150 | 151 | Uh-oh! Looks like you need to select an underlying and an expiration date first. 152 | 153 | ) 154 | } 155 | 156 | render() { 157 | return( 158 | <> 159 | 160 | {this.state.loading ? : ""} 161 | 162 | 163 | 164 | 168 | 169 | 170 | 171 | 172 | 173 | 177 | 178 | 179 | 180 | 181 |
182 | 183 | } 185 | > 186 | 187 | Option Table 188 | 189 | 190 | 191 | 192 | 193 | {!this.state.calls[0] ? this.errorMessage() : 194 | 204 | } 205 | 206 | 207 | 208 | 209 | 210 | } 212 | > 213 | 214 | Implied Volatility Skew 215 | 216 | 217 | 218 | 219 | {!this.state.calls[0] ? this.errorMessage() : 220 | <> 221 | 222 | 223 |
224 |
IV skew for  225 |
226 | {this.state.symbol} {this.state.expiry} 227 |
228 |  expiry option chain
229 |
230 | 231 |
232 | 233 | } 234 |
235 |
236 |
237 | 238 | } 240 | > 241 | 242 | Greeks 243 | 244 | 245 | 246 | 247 | 248 | 249 | {!this.state.calls[0]? "" : 250 |
251 | 258 |
259 | } 260 |
261 | 262 | {!this.state.calls[0] ? this.errorMessage() : 263 | <> 264 | 265 | 266 |
267 |
268 |
Greeks for  269 |
270 | {this.state.symbol} {this.state.expiry} 271 |
272 |  expiry option chain (rounded to 2 places)
273 |
274 | 279 |
280 | 281 | } 282 |
283 |
284 |
285 |
286 | 287 | ) 288 | } 289 | } 290 | 291 | export default OptionChain; --------------------------------------------------------------------------------