├── .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 |
24 |
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 |
24 |
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 | You need to enable JavaScript to run this app.
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 |
65 |
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 |
29 | Positions
30 |
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 |
52 | {displayQuantity(index)}
53 |
54 |
55 | {option["description"]}
56 |
57 |
58 | {
63 | props.onRemovePosition(index);
64 | }}
65 | >
66 | Remove
67 |
68 |
69 |
70 | );
71 | })}
72 |
73 |
74 |
75 |
76 | Close
77 |
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 {item} ;
30 | })}
31 |
32 |
33 |
34 | {props.calls.map((option, index) => (
35 |
42 | ))}
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 | Strike
51 |
52 |
53 |
54 | {props.calls.map(option => (
55 |
56 | {option.strike}
57 |
58 | ))}
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 | {props.layout.map((item, idx) => {
67 | return {item} ;
68 | })}
69 |
70 |
71 |
72 | {props.puts.map((option, index) => (
73 |
80 | ))}
81 |
82 |
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 |
32 | Edit Layout
33 |
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 | {item}
56 |
57 | {
62 | props.onAddLayoutItem(index);
63 | }}
64 | >
65 |
66 |
67 |
68 |
69 | );
70 | })}
71 |
72 |
73 |
74 |
75 |
76 | Current Layout
77 |
78 |
79 |
80 | {props.layout.map((item, index) => {
81 | return (
82 |
83 | {item}
84 |
85 | {
90 | props.onRemoveLayoutItem(index);
91 | }}
92 | >
93 |
94 |
95 |
96 |
97 | {
100 | props.onMoveUp(index);
101 | }}
102 | >
103 |
104 |
105 | {
108 | props.onMoveDown(index);
109 | }}
110 | >
111 |
112 |
113 |
114 |
115 |
116 | );
117 | })}
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 | Close
126 |
127 |
128 | {
132 | setShow(false);
133 | props.onSaveLayout();
134 | }}
135 | >
136 | Save
137 |
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 |
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 | Bid
134 |
135 | {props.option.bid}
136 |
137 |
138 |
139 | Ask
140 |
141 | {props.option.ask}
142 |
143 |
144 |
145 | Open
146 |
147 | {props.option.open}
148 |
149 |
150 |
151 | Previous Close
152 |
153 | {props.option.prevclose}
154 |
155 |
156 |
157 | Open Interest
158 |
159 | {props.option.open_interest}
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 | Implied Volatility
171 |
172 | {props.option.greeks.mid_iv}
173 |
174 |
175 |
176 | Delta
177 |
178 | {props.option.greeks.delta}
179 |
180 |
181 |
182 | Gamma
183 |
184 | {props.option.greeks.gamma}
185 |
186 |
187 |
188 | Theta
189 |
190 | {props.option.greeks.theta}
191 |
192 |
193 |
194 | Vega
195 |
196 | {props.option.greeks.vega}
197 |
198 |
199 |
200 |
201 |
202 |
203 |
204 |
205 |
206 |
207 |
208 |
217 |
218 |
219 |
220 | add()}
226 | >
227 | {quantity >= 0 ? "Buy" : "Sell"}
228 |
229 |
230 |
233 |
234 | Close
235 |
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 |
255 | calls
256 | puts
257 |
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;
--------------------------------------------------------------------------------