├── client ├── public │ ├── robots.txt │ ├── favicon.ico │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── index.html ├── src │ ├── Components │ │ ├── Pages │ │ │ ├── PageConstants.js │ │ │ ├── images │ │ │ │ ├── RCOSlogo.png │ │ │ │ ├── BrokerBot.png │ │ │ │ ├── alpacaLogo.png │ │ │ │ ├── github-logo.png │ │ │ │ └── stocks.svg │ │ │ ├── HomePage │ │ │ │ └── index.js │ │ │ └── LoginPage │ │ │ │ ├── LoginRegisterContent.js │ │ │ │ ├── FormTextInput.js │ │ │ │ ├── LoginBar.js │ │ │ │ ├── RegisterBar.js │ │ │ │ └── index.js │ │ └── Routes │ │ │ ├── PublicRoute.js │ │ │ └── PrivateRoute.js │ ├── redux │ │ ├── store.js │ │ └── userSlice.js │ ├── index.js │ ├── AppRouter.js │ └── style.css ├── package.json └── README.md ├── files └── Symbols.csv ├── src ├── ENUMS.py ├── RL.py ├── MainControl.py ├── Searcher.py ├── Strategy.py ├── BrokerBot.py ├── StrategyHandler.py ├── DataHandler.py ├── Factory.py ├── PortfolioManager.py └── ExecutionHandler.py ├── README.md ├── LICENSE ├── test ├── test_proxy2.py ├── test_bt_graphing.py ├── test_proxy.py ├── test_bt_analyzers.py └── data │ └── AAPL.csv └── .gitignore /client/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /client/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JackMansfield2019/BrokerBot/HEAD/client/public/favicon.ico -------------------------------------------------------------------------------- /client/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JackMansfield2019/BrokerBot/HEAD/client/public/logo192.png -------------------------------------------------------------------------------- /client/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JackMansfield2019/BrokerBot/HEAD/client/public/logo512.png -------------------------------------------------------------------------------- /client/src/Components/Pages/PageConstants.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | export const LoginPageScreenPosition = {Left: 0, Right: 35} -------------------------------------------------------------------------------- /client/src/Components/Pages/images/RCOSlogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JackMansfield2019/BrokerBot/HEAD/client/src/Components/Pages/images/RCOSlogo.png -------------------------------------------------------------------------------- /files/Symbols.csv: -------------------------------------------------------------------------------- 1 | Symbols 2 | GOOGL 3 | FB 4 | AMZN 5 | MSFT 6 | AAPL 7 | TSLA 8 | TSM 9 | BABA 10 | JPM 11 | JNJ 12 | WMT 13 | NVDA 14 | BAC 15 | -------------------------------------------------------------------------------- /client/src/Components/Pages/images/BrokerBot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JackMansfield2019/BrokerBot/HEAD/client/src/Components/Pages/images/BrokerBot.png -------------------------------------------------------------------------------- /client/src/Components/Pages/images/alpacaLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JackMansfield2019/BrokerBot/HEAD/client/src/Components/Pages/images/alpacaLogo.png -------------------------------------------------------------------------------- /client/src/Components/Pages/images/github-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JackMansfield2019/BrokerBot/HEAD/client/src/Components/Pages/images/github-logo.png -------------------------------------------------------------------------------- /client/src/redux/store.js: -------------------------------------------------------------------------------- 1 | import { configureStore } from '@reduxjs/toolkit'; 2 | import userReducer from './userSlice'; 3 | 4 | 5 | // Use ES6 object literal shorthand syntax to define the object shape 6 | export default configureStore({ 7 | reducer: { 8 | userData: userReducer, 9 | } 10 | }) 11 | -------------------------------------------------------------------------------- /client/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { Provider } from 'react-redux'; 4 | import AppRouter from './AppRouter'; 5 | import reduxStore from './redux/store' 6 | import './style.css'; 7 | 8 | //Need to add the redux store in Provider Tag 9 | ReactDOM.render( 10 | 11 | 12 | , 13 | document.getElementById('root') 14 | ); -------------------------------------------------------------------------------- /client/src/Components/Pages/HomePage/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | 4 | class HomePage extends React.Component { 5 | constructor(props){ 6 | super(props); 7 | 8 | this.state = { 9 | /*Fill in the state variables later with Broker Data needed for graphs and bots */ 10 | } 11 | } 12 | 13 | 14 | render(){ 15 | return <> 16 | } 17 | } 18 | 19 | export default connect()(HomePage); -------------------------------------------------------------------------------- /client/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /client/src/redux/userSlice.js: -------------------------------------------------------------------------------- 1 | import { createSlice } from '@reduxjs/toolkit'; 2 | 3 | export const userSlice = createSlice({ 4 | name: 'userData', 5 | initialState: { 6 | userID: '', 7 | userEmail: '', 8 | isAuthenticated: false, 9 | }, 10 | reducers: { 11 | userLogin: (state, actions) => { 12 | state.userEmail = actions.payload.email; 13 | state.userID = actions.payload.ID; 14 | state.isAuthenticated = true; 15 | } 16 | } 17 | 18 | 19 | }); 20 | 21 | 22 | export const { userLogin } = userSlice.actions; 23 | export default userSlice.reducer; -------------------------------------------------------------------------------- /client/src/Components/Routes/PublicRoute.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Route, Redirect, RouteProps } from 'react-router-dom'; 3 | //import { ROUTES } from '../constants'; 4 | 5 | interface PublicRouteProps { 6 | restricted?: boolean; 7 | } 8 | 9 | function PublicRoute(props: PublicRouteProps & RouteProps): React.ReactElement { 10 | const { component: Component, restricted = false, ...rest } = props; 11 | 12 | const render = props => { 13 | if ( restricted ) { 14 | return ; 15 | } 16 | 17 | return ; 18 | }; 19 | 20 | return ; 21 | } 22 | 23 | export default PublicRoute; -------------------------------------------------------------------------------- /client/src/Components/Routes/PrivateRoute.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Route, Redirect, RouteProps } from 'react-router-dom'; 3 | import { connect } from 'react-redux'; 4 | //import { ROUTES } from '/routes'; 5 | 6 | function PrivateRoute(props: RouteProps): React.ReactElement { 7 | const { component: Component, ...rest } = props; 8 | 9 | const render = props => { 10 | if (!props.isAuthenticated) { 11 | return ; 12 | } 13 | 14 | return ; 15 | }; 16 | 17 | return ; 18 | } 19 | 20 | const MapStateToProps = (state) => ({ 21 | isAuthenticated: state.userData.isAuthenticated 22 | }); 23 | 24 | export default connect(MapStateToProps)(PrivateRoute); -------------------------------------------------------------------------------- /client/src/AppRouter.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { BrowserRouter, Switch } from 'react-router-dom'; 3 | 4 | import PrivateRoute from './Components/Routes/PrivateRoute'; 5 | import PublicRoute from './Components/Routes/PublicRoute'; 6 | 7 | /*------------------------------ App Pages ------------------------------*/ 8 | import LoginPage from './Components/Pages/LoginPage/index'; 9 | import HomePage from './Components/Pages/HomePage/index'; 10 | 11 | 12 | 13 | class AppRouter extends React.Component { 14 | render() { 15 | return ( 16 | 17 | 18 | {/* ------------------ Public Routes Below ------------------- */} 19 | 20 | 21 | {/* ------------------ Private Routes Below ------------------ */} 22 | 23 | 24 | 25 | ); 26 | } 27 | } 28 | 29 | export default AppRouter; -------------------------------------------------------------------------------- /client/src/Components/Pages/LoginPage/LoginRegisterContent.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | export default function LoginRiegisterContent(props) { 5 | return
6 |
7 | 8 |
9 |
10 | 11 |
12 |
13 |
14 | Lorem ipsum dolor sit amet, consectetur adipiscing elit 15 |
16 |
17 | Ut enim ad minim veniam, quis nostrud 18 |
19 |
20 |
21 |
22 | } -------------------------------------------------------------------------------- /src/ENUMS.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | class Strategies(Enum): 4 | SHORT = 1 5 | MEDIUM = 2 6 | LONG = 3 7 | 8 | SHORT_LOW_RISK = 1 9 | MEDIUM_LOW_RISK = 2 10 | LONG_LOW_RISK = 3 11 | 12 | SHORT_MEDIUM_RISK = 4 13 | MEDIUM_MEDIUM_RISK = 5 14 | LONG_MEDIUM_RISK = 6 15 | 16 | SHORT_HIGH_RISK = 7 17 | MEDIUM_HIGH_RISK = 8 18 | LONG_HIGH_RISK = 9 19 | 20 | class Risk(Enum): 21 | SHORT_LOW_RISK = 1 22 | MEDIUM_LOW_RISK = 2 23 | LONG_LOW_RISK = 3 24 | 25 | SHORT_MEDIUM_RISK = 4 26 | MEDIUM_MEDIUM_RISK = 5 27 | LONG_MEDIUM_RISK = 6 28 | 29 | SHORT_HIGH_RISK = 7 30 | MEDIUM_HIGH_RISK = 8 31 | LONG_HIGH_RISK = 9 32 | 33 | 34 | class DH_API(Enum): 35 | ALPACA = 1 36 | 37 | BINANCE = 2 38 | POLYGON = 3 39 | IBKR = 4 40 | ALPHA = 5 41 | 42 | 43 | class EH_API(Enum): 44 | ALPACA = 1 45 | 46 | BINANCE = 2 47 | IBKR = 3 48 | ALPHA = 4 49 | 50 | class Stop_loss(Enum): 51 | RSI = 1 52 | STATIC = 2 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BrokerBot 2 | Broker Bot is an autonomous trading algorithm designed to continuously analyze New York Stock Exchange (NYSE) market conditions and execute profitable trades by utilizing advanced trading strategies. Built upon the Alpaca API, Broker Bot will be tuned through the extensive backtesting and paper trading capabilities provided. 3 | 4 | ## Installation Guide 5 | Before installing the following softwares, have the latest version of Python and Javascript(React) installed. Also note that these install commands are specific for the Ubuntu bash terminal. 6 |

To Make sure ubuntu is up to date: `$sudo apt update` 7 |

Step 1: Clone the repository locally onto your machine and open it up in your favorite editor 8 |
(For this Project we Reccomend VScode which can be found here: https://code.visualstudio.com/). 9 |

Step 2: Use the command `cd client` to go to the client directory 10 |

Step 3: Use the following commands to install the packages onto your machine `npm install` 11 |

Step 4: Start the application by running `npm run start` 12 | -------------------------------------------------------------------------------- /client/src/Components/Pages/LoginPage/FormTextInput.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default function FormTextInput(props) { 4 | return
5 |
{props.Variables.title}
6 |
7 | props.Functions.updateText(e)} autoComplete="off" spellCheck="false" style={{background: props.Variables.backgroundColor}} required > 9 | 10 |
11 | {props.Variables.showForgotPassword ?
12 | 13 |
: <>} 14 |
15 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 JackMansfield2019 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 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@reduxjs/toolkit": "^1.5.1", 7 | "@testing-library/jest-dom": "^5.12.0", 8 | "@testing-library/react": "^11.2.7", 9 | "@testing-library/user-event": "^12.8.3", 10 | "react": "^17.0.2", 11 | "react-dom": "^17.0.2", 12 | "react-icons": "^4.2.0", 13 | "react-redux": "^7.2.4", 14 | "react-router-dom": "^5.2.0", 15 | "react-scripts": "4.0.3", 16 | "web-vitals": "^1.1.2" 17 | }, 18 | "scripts": { 19 | "start": "react-scripts start", 20 | "build": "react-scripts build", 21 | "test": "react-scripts test", 22 | "eject": "react-scripts eject" 23 | }, 24 | "eslintConfig": { 25 | "extends": [ 26 | "react-app", 27 | "react-app/jest" 28 | ] 29 | }, 30 | "browserslist": { 31 | "production": [ 32 | ">0.2%", 33 | "not dead", 34 | "not op_mini all" 35 | ], 36 | "development": [ 37 | "last 1 chrome version", 38 | "last 1 firefox version", 39 | "last 1 safari version" 40 | ] 41 | }, 42 | "devDependencies": { 43 | "redux-devtools": "^3.7.0" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /test/test_proxy2.py: -------------------------------------------------------------------------------- 1 | import threading 2 | import websocket 3 | import json 4 | 5 | 6 | def on_open(ws): 7 | print("opened") 8 | auth_data = { 9 | "action": "authenticate", 10 | "data": {"key_id": "INSERT_KEY", "secret_key": "INSERT_KEY"} 11 | } 12 | 13 | ws.send(json.dumps(auth_data)) 14 | 15 | listen_message = {"action": "listen", "data": {"streams": ["T.TSLA"]}} 16 | 17 | ws.send(json.dumps(listen_message)) 18 | 19 | 20 | def on_message(ws, message): 21 | print("received a message") 22 | print(message) 23 | 24 | 25 | def on_close(ws): 26 | print("closed connection") 27 | 28 | 29 | socket = "ws://127.0.0.1:8765" 30 | 31 | # ws = websocket.WebSocketApp(socket, on_open=on_open, 32 | # on_message=on_message, on_close=on_close) 33 | 34 | ws = websocket.WebSocketApp(socket, 35 | on_message=lambda ws, msg: on_message(ws, 36 | msg), 37 | on_close=lambda ws: on_close(ws), 38 | on_open=lambda ws: on_open(ws)) 39 | 40 | 41 | t1 = threading.Thread(target=ws.run_forever, args=()) 42 | t1.start() 43 | -------------------------------------------------------------------------------- /test/test_bt_graphing.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Backtrader Plotting - example taken from API docs and modified 3 | - takes in data from... 4 | * GenericCSVData * 5 | VisualChartCSVData 6 | YahooFinanceData (for online downloads) 7 | * YahooFinanceCSVData (for already downloaded data) * 8 | BacktraderCSVData (in-house … for testing purposed, but can be used) 9 | 10 | YahooFinanceCSVData has similar format to our DH, cols are 11 | Date Open High Low Close Adj Close Volume 12 | 13 | - essentially combines backtrader with matplotlib to make it easier to plot 14 | strategies and results 15 | - can use a number of technical indicators through bt.Strategy, not sure how this would 16 | integrate with our classes 17 | 18 | ''' 19 | 20 | from __future__ import (absolute_import, division, print_function, 21 | unicode_literals) 22 | 23 | import backtrader as bt 24 | 25 | 26 | class St(bt.Strategy): 27 | def __init__(self): 28 | self.sma = bt.indicators.SimpleMovingAverage(self.data) 29 | 30 | 31 | data = bt.feeds.YahooFinanceCSVData(dataname='data/AAPL.csv') 32 | 33 | cerebro = bt.Cerebro() 34 | cerebro.adddata(data) 35 | cerebro.addstrategy(St) 36 | cerebro.run() 37 | cerebro.plot() 38 | -------------------------------------------------------------------------------- /test/test_proxy.py: -------------------------------------------------------------------------------- 1 | import threading 2 | import websocket 3 | import json 4 | import time 5 | 6 | 7 | def on_open(ws): 8 | print("opened") 9 | auth_data = { 10 | "action": "authenticate", 11 | "data": {"key_id": "INSERT_KEY", "secret_key": "INSERT_KEY"} 12 | } 13 | 14 | ws.send(json.dumps(auth_data)) 15 | 16 | listen_message = {"action": "listen", "data": {"streams": ["T.TSLA"]}} 17 | 18 | ws.send(json.dumps(listen_message)) 19 | 20 | 21 | def on_message(ws, message): 22 | print("WS 1: received a message") 23 | print(message) 24 | print() 25 | 26 | 27 | def on_message2(ws, message): 28 | print("WS 2: received a message") 29 | print(message) 30 | print() 31 | 32 | 33 | def on_close(ws): 34 | print("closed connection") 35 | 36 | 37 | socket = "ws://127.0.0.1:8765" 38 | 39 | # ws = websocket.WebSocketApp(socket, on_open=on_open, 40 | # on_message=on_message, on_close=on_close) 41 | 42 | ws = websocket.WebSocketApp(socket, 43 | on_message=lambda ws, msg: on_message(ws, 44 | msg), 45 | on_close=lambda ws: on_close(ws), 46 | on_open=lambda ws: on_open(ws)) 47 | 48 | ws2 = websocket.WebSocketApp(socket, 49 | on_message=lambda ws, msg: on_message2(ws, 50 | msg), 51 | on_close=lambda ws: on_close(ws), 52 | on_open=lambda ws: on_open(ws)) 53 | 54 | 55 | t1 = threading.Thread(target=ws.run_forever, args=()) 56 | t2 = threading.Thread(target=ws2.run_forever, args=()) 57 | 58 | t1.start() 59 | t2.start() 60 | 61 | time.sleep(10) 62 | t1.kill() 63 | t2.kill() 64 | 65 | print("joined") 66 | -------------------------------------------------------------------------------- /client/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | #Front End 2 | # dependencies 3 | /client/node_modules 4 | 5 | # Byte-compiled / optimized / DLL files 6 | __pycache__/ 7 | *.py[cod] 8 | *$py.class 9 | 10 | # C extensions 11 | *.so 12 | 13 | # Distribution / packaging 14 | .Python 15 | build/ 16 | develop-eggs/ 17 | dist/ 18 | downloads/ 19 | eggs/ 20 | .eggs/ 21 | lib/ 22 | lib64/ 23 | parts/ 24 | sdist/ 25 | var/ 26 | wheels/ 27 | pip-wheel-metadata/ 28 | share/python-wheels/ 29 | *.egg-info/ 30 | .installed.cfg 31 | *.egg 32 | MANIFEST 33 | 34 | # PyInstaller 35 | # Usually these files are written by a python script from a template 36 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 37 | *.manifest 38 | *.spec 39 | 40 | # Installer logs 41 | pip-log.txt 42 | pip-delete-this-directory.txt 43 | 44 | # Unit test / coverage reports 45 | htmlcov/ 46 | .tox/ 47 | .nox/ 48 | .coverage 49 | .coverage.* 50 | .cache 51 | nosetests.xml 52 | coverage.xml 53 | *.cover 54 | *.py,cover 55 | .hypothesis/ 56 | .pytest_cache/ 57 | 58 | # Translations 59 | *.mo 60 | *.pot 61 | 62 | # Django stuff: 63 | *.log 64 | local_settings.py 65 | db.sqlite3 66 | db.sqlite3-journal 67 | 68 | # Flask stuff: 69 | instance/ 70 | .webassets-cache 71 | 72 | # Scrapy stuff: 73 | .scrapy 74 | 75 | # Sphinx documentation 76 | docs/_build/ 77 | 78 | # PyBuilder 79 | target/ 80 | 81 | # Jupyter Notebook 82 | .ipynb_checkpoints 83 | 84 | # IPython 85 | profile_default/ 86 | ipython_config.py 87 | 88 | # pyenv 89 | .python-version 90 | 91 | # pipenv 92 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 93 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 94 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 95 | # install all needed dependencies. 96 | #Pipfile.lock 97 | 98 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 99 | __pypackages__/ 100 | 101 | # Celery stuff 102 | celerybeat-schedule 103 | celerybeat.pid 104 | 105 | # SageMath parsed files 106 | *.sage.py 107 | 108 | # Environments 109 | .env 110 | .venv 111 | env/ 112 | venv/ 113 | ENV/ 114 | env.bak/ 115 | venv.bak/ 116 | 117 | # Spyder project settings 118 | .spyderproject 119 | .spyproject 120 | 121 | # Rope project settings 122 | .ropeproject 123 | 124 | # mkdocs documentation 125 | /site 126 | 127 | # mypy 128 | .mypy_cache/ 129 | .dmypy.json 130 | dmypy.json 131 | 132 | # Pyre type checker 133 | .pyre/ 134 | 135 | src/secrets.py 136 | scr/config.py 137 | -------------------------------------------------------------------------------- /client/src/Components/Pages/LoginPage/LoginBar.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { HiOutlineMail } from "react-icons/hi"; 3 | import { HiOutlineLockClosed } from "react-icons/hi"; 4 | import FormTextInput from './FormTextInput' 5 | //import { Link } from 'react-router-dom'; 6 | //Work on the redirect o the Home Page 7 | 8 | var CURRENT_ID = 0; 9 | 10 | export default function LoginBar(props) { 11 | 12 | return
13 |
14 |
15 | 16 |
17 |
18 | 19 |
20 |
21 |
22 |
23 | Welcome Back! 24 |
25 |
26 | Sign into your account 27 |
28 |
29 |
props.Functions.loginSubmitClicked(e)}> 30 |
31 | 34 | 35 | 36 | 39 | 40 | 41 |
42 | 43 |
44 |
45 |
46 |
47 | {props.Variables.Icons.map((item) => { 48 | return 49 | 50 | ; 51 | })} 52 |
53 |
54 |
55 |
56 |
57 | } -------------------------------------------------------------------------------- /client/src/Components/Pages/LoginPage/RegisterBar.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { HiOutlineMail } from "react-icons/hi"; 3 | import { HiOutlineLockClosed } from "react-icons/hi"; 4 | 5 | import FormTextInput from './FormTextInput' 6 | 7 | var CURRENT_ID = 0; 8 | 9 | export default function RegisterBar(props) { 10 | 11 | return
12 |
13 |
14 | 15 |
16 |
17 | 18 |
19 |
20 |
21 |
22 | Welcome! 23 |
24 |
25 | Sign up for your new account today 26 |
27 |
28 | {/* From here down not finished */} 29 |
props.Functions.registerSubmitClicked(e)}> 30 |
31 | 34 | 35 | 36 | 39 | 40 | 41 | 44 | 45 | 46 |
47 | 48 |
49 |
50 |
51 |
52 | {props.Variables.Icons.map((item) => { 53 | return 54 | 55 | ; 56 | })} 57 |
58 |
59 |
60 |
61 | 62 | } 63 | -------------------------------------------------------------------------------- /client/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Create React App 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 4 | 5 | ## Available Scripts 6 | 7 | In the project directory, you can run: 8 | 9 | ### `npm start` 10 | 11 | Runs the app in the development mode.\ 12 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 13 | 14 | The page will reload if you make edits.\ 15 | You will also see any lint errors in the console. 16 | 17 | ### `npm test` 18 | 19 | Launches the test runner in the interactive watch mode.\ 20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 21 | 22 | ### `npm run build` 23 | 24 | Builds the app for production to the `build` folder.\ 25 | It correctly bundles React in production mode and optimizes the build for the best performance. 26 | 27 | The build is minified and the filenames include the hashes.\ 28 | Your app is ready to be deployed! 29 | 30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 31 | 32 | ### `npm run eject` 33 | 34 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 35 | 36 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 37 | 38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 39 | 40 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 41 | 42 | ## Learn More 43 | 44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 45 | 46 | To learn React, check out the [React documentation](https://reactjs.org/). 47 | 48 | ### Code Splitting 49 | 50 | This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting) 51 | 52 | ### Analyzing the Bundle Size 53 | 54 | This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size) 55 | 56 | ### Making a Progressive Web App 57 | 58 | This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app) 59 | 60 | ### Advanced Configuration 61 | 62 | This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration) 63 | 64 | ### Deployment 65 | 66 | This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment) 67 | 68 | ### `npm run build` fails to minify 69 | 70 | This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify) 71 | -------------------------------------------------------------------------------- /src/RL.py: -------------------------------------------------------------------------------- 1 | class Trader(): 2 | def __init__(self, state_size, is_eval=False, model_name=""): 3 | self.state_size = state_size # normalized previous days 4 | self.action_size = 3 # hold, buy, sell 5 | self.memory = deque(maxlen=1000) 6 | self.inventory = [] 7 | self.model_name = model_name 8 | self.is_eval = is_eval 9 | self.gamma = 0.95 10 | self.epsilon = 1.0 11 | self.epsilon_min = 0.01 12 | self.epsilon_decay = 0.995 13 | self.model = load_model(model_name) if is_eval else self._model() 14 | 15 | def _model(self): 16 | model = Sequential() 17 | model.add(Dense(units=64, input_dim=self.state_size, activation="relu")) 18 | model.add(Dense(units=32, activation="relu")) 19 | model.add(Dense(units=8, activation="relu")) 20 | model.add(Dense(self.action_size, activation="linear")) 21 | model.compile(loss="mse", optimizer=Adam(lr=0.001)) 22 | return model 23 | 24 | def act(self, state): 25 | if not self.is_eval and random.random() <= self.epsilon: 26 | return random.randrange(self.action_size) 27 | options = self.model.predict(state) 28 | return np.argmax(options[0]) 29 | 30 | def expReplay(self, batch_size): 31 | mini_batch = [] 32 | l = len(self.memory) 33 | for i in range(l - batch_size + 1, l): 34 | mini_batch.append(self.memory[i]) 35 | for state, action, reward, next_state, done in mini_batch: 36 | target = reward 37 | if not done: 38 | target = reward + self.gamma * np.amax(self.model.predict(next_state)[0]) 39 | target_f = self.model.predict(state) 40 | target_f[0][action] = target 41 | self.model.fit(state, target_f, epochs=1, verbose=0) 42 | if self.epsilon > self.epsilon_min: 43 | self.epsilon *= self.epsilon_decay 44 | 45 | 46 | class RL(Strategy): 47 | # need to get the stock data from data handler 48 | def getStockDataVec(key): 49 | data = [] 50 | #rsi = [] 51 | #lines = open(key+".csv","r").read().splitlines() 52 | for line in lines[15:]: 53 | data.append(float(line.split(",")[1])) 54 | #rsi.append(float(line.split(",")[8])) 55 | #return data, rsi 56 | return data 57 | 58 | def sigmoid(x): 59 | return 1/(1+math.exp(-x)) 60 | 61 | 62 | # cerebro gives us next data in dataframe, so need to incorporate cerebro in order to get the state 63 | def getState(data, t, n): #getState(data, rsi, t, n) 64 | d = t - n + 1 65 | if d >= 0: 66 | block1 = data[d:t+1] 67 | #block2 = rsi[d:t+1] 68 | else: 69 | block1 = -d * [data[0]] + data[0:t+1] # pad with t0 70 | #block2 = -d * [rsi[0]] + rsi[0:t+1] # pad with t0 71 | res = [] 72 | for i in range(n-1): 73 | res.append((sigmoid(block1[i+1] - block1[i]))) 74 | #res.append((sigmoid(block1[i+1] - block1[i])) + (sigmoid(block2[i+1] - block2[i]))) 75 | return np.array([res]) 76 | 77 | """ 78 | def formatPrice(n): 79 | return("-$" if n < 0 else "$") + "{0:.2f}".format(abs(n)) 80 | """ 81 | 82 | def run(): 83 | # set as default 84 | window_size = 30 85 | 86 | trader = Trader(window_size) 87 | 88 | # set as default 89 | batch_size = 32 90 | 91 | # need to figure out what terminated should be 92 | while(not terminated): 93 | data = self.getStockDataVec(key): 94 | state = getState(data, t, window_size + 1) # need to fix t 95 | ction = trader.act(state) 96 | 97 | # hold 98 | next_state = getState(data, t + 1, window_size + 1) 99 | reward = 0 100 | 101 | #--------------------------------------------------------------------------------------------------- -------------------------------------------------------------------------------- /src/MainControl.py: -------------------------------------------------------------------------------- 1 | from BrokerBot import BrokerBot 2 | from Searcher import Searcher 3 | from multiprocessing import Process, Pipe 4 | # import Searcher 5 | import datetime 6 | import pytz # pip 7 | import holidays # pip 8 | import os 9 | import threading 10 | from dotenv import load_dotenv # pip 11 | import time 12 | 13 | DEBUG = True 14 | 15 | 16 | class MainControl: 17 | 18 | # TODO: figure out config format and proper parseing technique 19 | # TODO: make BB and searcher extend thread class 20 | def __init__(self): 21 | self.api_key = "" 22 | self.secret_key = "" 23 | self.base_url = "" 24 | self.socket = "" 25 | self.broker_bots = [] 26 | self.searchers = [] 27 | 28 | try: 29 | self.api_key = os.environ['API_KEY'] 30 | self.secret_key = os.environ['SECRET_KEY'] 31 | self.base_url = os.environ['BASE_URL'] 32 | self.socket = os.environ['SOCKET'] 33 | except Exception: 34 | load_dotenv() 35 | self.api_key = os.getenv('API_KEY') 36 | self.secret_key = os.getenv('SECRET_KEY') 37 | self.base_url = os.getenv('BASE_URL') 38 | self.socket = os.getenv('SOCKET') 39 | 40 | bb_conn, search_conn = Pipe() 41 | self.broker_bots.append(BrokerBot( 42 | self.api_key, self.secret_key, self.base_url, self.socket, bb_conn)) 43 | 44 | # self.searchers.append(Searcher( 45 | # self.api_key, self.secret_key, self.base_url, self.socket, search_conn)) 46 | 47 | def run(self): 48 | bb_proc = Process(target=self.broker_bots[0].run, args=()) 49 | # search_proc = Process(target=self.searchers[0].run, args=()) 50 | # TODO: Implement timing algo fully 51 | while market_closed and not DEBUG: 52 | print("MARKET CLOSED : SLEEPING FOR 1 MIN") 53 | time.sleep(60) 54 | continue 55 | 56 | 57 | # eventually make this loop starting bb and searchers for multiple users 58 | 59 | bb_proc.start() 60 | while True: 61 | if market_closed and not DEBUG: 62 | bb_proc.join() 63 | else: 64 | time.sleep(60) 65 | continue 66 | # search_proc.start() 67 | 68 | # def test_data_ingest(self): 69 | # # Goal : spin up several broker bots on different threads with same API key -> same socket 70 | # # see if proxy agent works with different socket instances w/ same key or need to 71 | # # instanitnate bb/searcher with one ws object 72 | # broker_bots = [] 73 | # listening_threads = [] 74 | # tickers = ["TSLA", "AAPL", "GME", "AMC", "ROKU"] 75 | # for i in range(2): 76 | # broker_bots.append( 77 | # BrokerBot(self.api_key, self.secret_key, self.base_url, self.data_url)) 78 | 79 | # t1 = threading.Thread(target=broker_bots[0].test_stream_data, args=(tickers[0],)) 80 | # t2 = threading.Thread(target=broker_bots[1].test_stream_data, args=(tickers[1],)) 81 | 82 | # t1.start() 83 | # t2.start() 84 | # # ticker_counter = 0 85 | # # for bot in broker_bots: 86 | # # stream_thread = threading.Thread( 87 | # # target=bot.test_stream_data, args=(tickers[ticker_counter],)) 88 | 89 | # # listening_threads.append(stream_thread) 90 | # # ticker_counter += 1 91 | 92 | # # for thread in listening_threads: 93 | # # thread.start() 94 | 95 | 96 | def market_closed(now=None): 97 | tz = pytz.timezone('US/Eastern') 98 | us_holidays = holidays.US() 99 | 100 | openTime = datetime.time(hour=9, minute=30, second=0) 101 | closeTime = datetime.time(hour=16, minute=0, second=0) 102 | 103 | if not now: 104 | now = datetime.datetime.now(tz) 105 | 106 | # If a holiday 107 | if now.strftime('%Y-%m-%d') in us_holidays: 108 | return True 109 | # If before 0930 or after 1600 110 | if (now.time() < openTime) or (now.time() > closeTime): 111 | return True 112 | # If it's a weekend 113 | if now.date().weekday() > 4: 114 | return True 115 | 116 | return False 117 | 118 | 119 | if __name__ == "__main__": 120 | main_control = MainControl() 121 | main_control.run() 122 | -------------------------------------------------------------------------------- /src/Searcher.py: -------------------------------------------------------------------------------- 1 | import websocket 2 | import json 3 | import numpy as np 4 | import pandas as pd 5 | import requests 6 | import math 7 | import alpaca_trade_api as tradeapi 8 | import time # used for calculating time 9 | from statistics import mean # used to calculate avg volume 10 | from enum import Enum 11 | 12 | class TimeFrame(Enum): 13 | ONE_MIN = "1Min" 14 | FIVE_MIN = "5Min" 15 | FIFTEEN_MIN = "15Min" 16 | ONE_HOUR = "1Hour" 17 | ONE_DAY = "1Day" 18 | 19 | class Searcher: 20 | def __init__(self, API_key_id, API_secret_key, base_url, socket, strat_conns): 21 | self.headers = {"APCA-API-KEY-ID": API_key_id,"APCA-API-SECRET-KEY": API_secret_key} 22 | self.base_url = base_url 23 | self.account_url= "{}/v2/account".format(self.base_url) 24 | self.order_url = "{}/v2/orders".format(self.base_url) 25 | self.strat_conns = strat_conns 26 | self.strat_counter = 0 27 | self.stock_set = set() 28 | self.api = tradeapi.REST( 29 | self.headers["APCA-API-KEY-ID"], 30 | self.headers["APCA-API-SECRET-KEY"], 31 | base_url 32 | ) 33 | # self.api_account = api.get_account() 34 | self.socket = socket 35 | self.DH = AlpacaDataHandler(API_key_id, API_secret_key, base_url) 36 | 37 | """ 38 | Columns = ['Ticker', 'Time', 'Volume'] 39 | self.dataframe = pd.DataFrame(columns = Columns) 40 | self.stock_data = dataframe.set_index("Ticker", drop = False) 41 | 42 | # sets the time for each stock to the time we first initialize the searcher. 43 | for stock in self.stocks: 44 | t = int(time.time()) 45 | self.stock_data = self.stock_data.append( pd.Series([ stock, t], index = cols ), ignore_index = True) 46 | """ 47 | self.stocks = pd.read_csv("files/Symbols.csv") 48 | time = int(time.time()) 49 | stock_data = {} 50 | for stock in stocks: 51 | stock_data[stock] = time 52 | #self.queue = [[]] # priority queue 53 | 54 | def get_account(self): 55 | return account 56 | 57 | """ 58 | Overview: updates the priority of each stock, THIS IS THE RUN METHOD OF SCREENER 59 | Effects: updates the weights of the priority queue's stocks 60 | """ 61 | def search(self): 62 | for stock in self.stocks: 63 | time_initial = stock_data[stock] 64 | time_final, stock_volume = self.get_data(stock, time_initial, TimeFrame.FIVE_MIN) 65 | stock_data[stock] = time_final 66 | self.ACV(stock_volume, stock) 67 | 68 | 69 | """ 70 | Overview: returns the previous 5-minute-volume for the given stock by the client 71 | Returns: volume of the stock that was passed in 72 | Throws: 73 | - Exception if time_initial < 0 74 | - Exception if stock is None/Null 75 | N.B.: Ticker Limit per API Request = 200 76 | """ 77 | def get_data(self, stock, time_initial, timeframe): 78 | if time_initial < 0: raise Exception("Time Initial cannot be < 0!") 79 | if stock is None or stock == "": raise Exception("stock cannot be None/Null or blank!") 80 | 81 | time_current = int(time.time()) 82 | bars = int((time_current - time_initial) / 300) 83 | barset = DH.get_bars(stock, time_initial, time_current, timeframe, bars) 84 | 85 | #assuming the previous bar from current is in the last index of barset 86 | #last = len(barset) - 1 87 | #stock_time = barset[last][0] 88 | 89 | stock_volume = [] 90 | row_count = barset.shape[0] 91 | stock_time = time_current 92 | for i in range(0, row_count): 93 | stock_close.append(barset.iloc[i, 5]) 94 | return stock_time, stock_volume 95 | 96 | 97 | """ 98 | Overview: calculates the average change in volume 99 | Requires: volumes is not None 100 | Returns: updated priorities of each stock in stocks list 101 | """ 102 | def ACV(self, volumes, stock): 103 | acv = int(mean(volumes)) 104 | s = [acv, stock] 105 | length = len(self.queue) 106 | 107 | if len(self.queue) == 0: 108 | self.queue.append(s) 109 | else: 110 | for i in range(0,length): 111 | if s[0] > self.queue[i][0]: 112 | self.queue.insert(i, s) 113 | break 114 | elif s[0] < self.queue[i][0] and i == length - 1: 115 | self.queue.append(s) 116 | 117 | """ 118 | # *** gets the data from the broker bot priority queue *** 119 | 120 | def set_socket(self,socket = "wss://data.alpaca.markets/stream"): 121 | self.socket = socket 122 | 123 | def submit_order(self,symbol, qty, side, type, time_in_force, limit_price=None, stop_price=None, 124 | client_order_id=None, order_class=None, take_profit=None, stop_loss=None, 125 | trail_price=None, trail_percent=None): 126 | 127 | api.submit_order(symbol, qty, side, type, time_in_force, limit_price, stop_price, 128 | client_order_id, order_class, take_profit, stop_loss, 129 | trail_price, trail_percent) 130 | 131 | def on_open(): 132 | print("opened-stream") 133 | auth_data = { 134 | "action": "authenticate", 135 | "data": {"key_id": config.API_KEY, "secret_key": config.SECRET_KEY} 136 | } 137 | 138 | ws.send(json.dumps(auth_data)) 139 | 140 | listen_message = {"action": "listen", "data": {"streams": ["AM.TSLA"]}} 141 | 142 | ws.send(json.dumps(listen_message)) 143 | 144 | 145 | def on_message(ws, message): 146 | print("received a message") 147 | print(message) 148 | 149 | def on_close(ws): 150 | print("closed connection") 151 | 152 | socket = "wss://data.alpaca.markets/stream" 153 | 154 | ws = websocket.WebSocketApp(socket, on_open=on_open, on_message=on_message, on_close=on_close) 155 | ws.run_forever() 156 | """ -------------------------------------------------------------------------------- /client/src/Components/Pages/LoginPage/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import MainImage from '../images/stocks.svg'; 3 | import Logo from '../images/BrokerBot.png'; 4 | 5 | /*------------------ React Redux --------------------*/ 6 | import { connect } from 'react-redux'; 7 | import { userLogin } from '../../../redux/userSlice'; 8 | /*-------------- Page Constant Values -------------- */ 9 | import * as Constants from '../PageConstants' 10 | 11 | /* ---------------- Icons for Links ---------------- */ 12 | import RCOS from '../images/RCOSlogo.png' 13 | import GIT from '../images/github-logo.png' 14 | import ALPACA from '../images/alpacaLogo.png' 15 | 16 | /* -------------- Components for Page -------------- */ 17 | import LoginBar from "./LoginBar" 18 | import LoginRegisterContent from "./LoginRegisterContent" 19 | import RegisterBar from "./RegisterBar" 20 | 21 | const LeftScreenPosition = Constants.LoginPageScreenPosition.Left; 22 | const RightScreenPosition = Constants.LoginPageScreenPosition.Right; 23 | 24 | class LoginPage extends React.Component { 25 | constructor(props) { 26 | super(props); 27 | 28 | this.state = { 29 | currentPosition: RightScreenPosition, 30 | email: '', 31 | password: '', 32 | confirmPassword: '', 33 | LoginFunctions: { moveScreenTo: this.moveScreenTo, 34 | LearnMorePressed: this.LearnMorePressed, 35 | loginSubmitClicked: this.loginSubmitClicked, 36 | updateText: this.updateText, 37 | forgotPasswordPressed: this.forgotPasswordPressed, 38 | }, 39 | RegisterFunctions: { moveScreenTo: this.moveScreenTo, 40 | LearnMorePressed: this.LearnMorePressed, 41 | registerSubmitClicked: this.registerSubmitClicked, 42 | updateText: this.updateText }, 43 | } 44 | } 45 | 46 | componentDidMount(){ 47 | console.log(this.props.userData); 48 | } 49 | 50 | componentDidUpdate(prevProps){ 51 | console.log(this.props.userData); 52 | } 53 | 54 | /** 55 | * Redirects the user to the forgot password page 56 | */ 57 | forgotPasswordPressed = () => { 58 | alert('Forgot Password was Pressed but functionality is not finished'); 59 | } 60 | 61 | /** 62 | * Redirects the user to the forgot password page 63 | */ 64 | LearnMorePressed = () => { 65 | alert('Forgot Password was Pressed but functionality is not finished'); 66 | } 67 | 68 | /** 69 | * Updates a text value to its new inputed value for input JSX tags 70 | * @param {[event]} e the event holding the variable and new text value 71 | */ 72 | updateText = e => { 73 | this.setState({ 74 | [e.target.name]: e.target.value 75 | }); 76 | } 77 | 78 | /** 79 | * Moves the login/register page screens view position resets the forms and forces an Update for the Component 80 | * @param {[Number]} position a value between 0 and 35 where 0 is the farthest left and 35 is the farthest right 81 | */ 82 | moveScreenTo = position => { 83 | this.setState({ 84 | ...this.state, 85 | currentPosition: position, 86 | email: '', 87 | password: '', 88 | confirmPassword: '', 89 | 90 | }) 91 | this.forceUpdate(); 92 | } 93 | 94 | /** 95 | * Sends a Request to the backend checking to see if the if the User and Password 96 | * match a known user in the database. If the User exists re-route them to their 97 | * account if they dont then keep them at he login screen 98 | * @param {[event]} e the event holding the form that was submitted and its relevant data 99 | */ 100 | loginSubmitClicked = event => { 101 | /*Check to see if Valid before prevent default*/ 102 | var AlertValue = `Email: ${this.state.email}\nPassword: ${this.state.password}`; 103 | alert(AlertValue); 104 | 105 | if(this.state.password === "TESTING"){ 106 | this.props.dispatch(userLogin({ 107 | ID: 1, 108 | email: this.state.email, 109 | })); 110 | } 111 | //console.log(event); 112 | event.preventDefault(); 113 | } 114 | 115 | /** 116 | * Sends a Request to the backend checking to see if the if the User email already exists in the database. 117 | * If the Request returns saying that the user has been made let them into their new account otherwise 118 | * keep them on this page. 119 | * @param {[event]} e the event holding the form that was submitted and its relevant data 120 | */ 121 | registerSubmitClicked = event => { 122 | var AlertValue = `Email: ${this.state.email}\nPassword: ${this.state.password}\nConfirm Password: ${this.state.confirmPassword}`; 123 | alert(AlertValue); 124 | event.preventDefault(); 125 | } 126 | 127 | render() { 128 | return ( 129 |
130 |
131 | 139 | 140 | 148 |
149 |
150 | ); 151 | } 152 | } 153 | 154 | const MapStateToProps = (state) => ({ 155 | userData: state.userData 156 | }); 157 | 158 | export default connect(MapStateToProps)(LoginPage); -------------------------------------------------------------------------------- /test/test_bt_analyzers.py: -------------------------------------------------------------------------------- 1 | ''' 2 | List of different Analyzers: 3 | AnnualReturn 4 | Calmar 5 | DrawDown 6 | TimeDrawDown 7 | GrossLeverage 8 | PositionsValue 9 | PyFolio 10 | LogReturnsRolling 11 | PeriodStats 12 | Returns 13 | SharpeRatio 14 | SharpeRatio_A 15 | SQN 16 | TimeReturn 17 | TradeAnalyzer 18 | Transactions 19 | VWR 20 | 21 | Pyfolio Integration - had trouble getting this running locally, 22 | however it is integrated into backtrader 23 | so it might be worth looking into more 24 | 25 | Single Stock Example: https://quantopian.github.io/pyfolio/notebooks/single_stock_example/ 26 | ''' 27 | 28 | # Example from Backtrader Analyzers docs, but modified 29 | 30 | from __future__ import (absolute_import, division, print_function, 31 | unicode_literals) 32 | 33 | 34 | import argparse 35 | import datetime 36 | import random 37 | 38 | import backtrader as bt 39 | 40 | 41 | class St(bt.Strategy): 42 | params = ( 43 | ('printout', False), 44 | ('stake', 1000), 45 | ) 46 | 47 | def __init__(self): 48 | pass 49 | 50 | def start(self): 51 | if self.p.printout: 52 | txtfields = list() 53 | txtfields.append('Len') 54 | txtfields.append('Datetime') 55 | txtfields.append('Open') 56 | txtfields.append('High') 57 | txtfields.append('Low') 58 | txtfields.append('Close') 59 | txtfields.append('Volume') 60 | txtfields.append('OpenInterest') 61 | print(','.join(txtfields)) 62 | 63 | def next(self): 64 | if self.p.printout: 65 | # Print only 1st data ... is just a check that things are running 66 | txtfields = list() 67 | txtfields.append('%04d' % len(self)) 68 | txtfields.append(self.data.datetime.datetime(0).isoformat()) 69 | txtfields.append('%.2f' % self.data0.open[0]) 70 | txtfields.append('%.2f' % self.data0.high[0]) 71 | txtfields.append('%.2f' % self.data0.low[0]) 72 | txtfields.append('%.2f' % self.data0.close[0]) 73 | txtfields.append('%.2f' % self.data0.volume[0]) 74 | txtfields.append('%.2f' % self.data0.openinterest[0]) 75 | print(','.join(txtfields)) 76 | 77 | # Data 0 78 | for data in self.datas: 79 | toss = random.randint(1, 10) 80 | curpos = self.getposition(data) 81 | if curpos.size: 82 | if toss > 5: 83 | size = curpos.size // 2 84 | self.sell(data=data, size=size) 85 | if self.p.printout: 86 | print('SELL {} @%{}'.format(size, data.close[0])) 87 | 88 | elif toss < 5: 89 | self.buy(data=data, size=self.p.stake) 90 | if self.p.printout: 91 | print('BUY {} @%{}'.format(self.p.stake, data.close[0])) 92 | 93 | 94 | def runstrat(args=None): 95 | args = parse_args(args) 96 | print(args) 97 | cerebro = bt.Cerebro() 98 | cerebro.broker.set_cash(args.cash) 99 | 100 | dkwargs = dict() 101 | if args.fromdate: 102 | fromdate = datetime.datetime.strptime(args.fromdate, '%Y-%m-%d') 103 | dkwargs['fromdate'] = fromdate 104 | 105 | if args.todate: 106 | todate = datetime.datetime.strptime(args.todate, '%Y-%m-%d') 107 | dkwargs['todate'] = todate 108 | 109 | data0 = bt.feeds.YahooFinanceCSVData(dataname=args.data0, **dkwargs) 110 | cerebro.adddata(data0, name='Data0') 111 | 112 | data1 = bt.feeds.YahooFinanceCSVData(dataname=args.data1, **dkwargs) 113 | cerebro.adddata(data1, name='Data1') 114 | 115 | data2 = bt.feeds.YahooFinanceCSVData(dataname=args.data2, **dkwargs) 116 | cerebro.adddata(data2, name='Data2') 117 | 118 | cerebro.addstrategy(St, printout=args.printout) 119 | if not args.no_pyfolio: 120 | cerebro.addanalyzer(bt.analyzers.PyFolio, _name='pyfolio') 121 | 122 | results = cerebro.run() 123 | if not args.no_pyfolio: 124 | strat = results[0] 125 | pyfoliozer = strat.analyzers.getbyname('pyfolio') 126 | 127 | returns, positions, transactions, gross_lev = pyfoliozer.get_pf_items() 128 | if args.printout: 129 | print('-- RETURNS') 130 | print(returns) 131 | print('-- POSITIONS') 132 | print(positions) 133 | print('-- TRANSACTIONS') 134 | print(transactions) 135 | print('-- GROSS LEVERAGE') 136 | print(gross_lev) 137 | 138 | import pyfolio as pf 139 | pf.create_full_tear_sheet( 140 | returns, 141 | positions=positions, 142 | transactions=transactions, 143 | gross_lev=gross_lev, 144 | live_start_date='2005-05-01', 145 | round_trips=True) 146 | 147 | if args.plot: 148 | cerebro.plot(style=args.plot_style) 149 | 150 | 151 | def parse_args(args=None): 152 | 153 | parser = argparse.ArgumentParser( 154 | formatter_class=argparse.ArgumentDefaultsHelpFormatter, 155 | description='Sample for pivot point and cross plotting') 156 | 157 | parser.add_argument('--data0', required=False, 158 | default='AAPL.csv', 159 | help='Data to be read in') 160 | 161 | parser.add_argument('--data1', required=False, 162 | default='TSLA.csv', 163 | help='Data to be read in') 164 | 165 | parser.add_argument('--data2', required=False, 166 | default='PLTR.csv', 167 | help='Data to be read in') 168 | 169 | parser.add_argument('--fromdate', required=False, 170 | default='2005-01-01', 171 | help='Starting date in YYYY-MM-DD format') 172 | 173 | parser.add_argument('--todate', required=False, 174 | default='2006-12-31', 175 | help='Ending date in YYYY-MM-DD format') 176 | 177 | parser.add_argument('--printout', required=False, action='store_true', 178 | help=('Print data lines')) 179 | 180 | parser.add_argument('--cash', required=False, action='store', 181 | type=float, default=50000, 182 | help=('Cash to start with')) 183 | 184 | parser.add_argument('--plot', required=False, action='store_true', 185 | help=('Plot the result')) 186 | 187 | parser.add_argument('--plot-style', required=False, action='store', 188 | default='bar', choices=['bar', 'candle', 'line'], 189 | help=('Plot style')) 190 | 191 | parser.add_argument('--no-pyfolio', required=False, action='store_true', 192 | help=('Do not do pyfolio things')) 193 | 194 | import sys 195 | aargs = args if args is not None else sys.argv[1:] 196 | return parser.parse_args(aargs) 197 | 198 | runstrat([]) 199 | -------------------------------------------------------------------------------- /src/Strategy.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod # Abstract class module for python. 2 | from DataHandler import DataHandler 3 | from ExecutionHandler import ExecutionHandler 4 | from backtrader import backtrader as bt 5 | from threading import Thread 6 | from multiprocessing import Process, Pipe 7 | from ENUMS import * 8 | from Factory import * 9 | import time 10 | import queue 11 | 12 | """ 13 | overview: 14 | - Strategy Class: Strategy is a base class for all strategies in BrokerBot. 15 | It takes in a DataHandler and an ExecutionHandler, which it uses to communicate 16 | with the given broker. (Broker type and trade type are set by the StrategyHandler) 17 | It then runs its strategy, which is logged continuously by Cerebro. This allows for 18 | backtesting and analysis of the stretegy after the fact. 19 | 20 | Requires: api_key,secret_key,base_url,data_url,socket, base_url, ticker, 21 | DH_API,EH_API, 22 | 23 | Modifies: DH, EH,log, 24 | Effects: DH is initialized, EH is initialized, stream is listend to, Log is initialized. 25 | Returns: none 26 | TODO: 27 | - Create more strategies. 28 | - Refine backtrading setup and functions. 29 | - 30 | """ 31 | class Strategy(ABC, bt.Strategy): 32 | @abstractmethod 33 | def __init__(self, dh: DataHandler, eh: ExecutionHandler, ticker: str, strat_search_conn): 34 | self.dh = dh 35 | self.eh = eh 36 | self.ticker = ticker 37 | self.dh_queue = None 38 | self.eh_conn = None 39 | self.queue = [] 40 | self.strat_search_conn = strat_search_conn 41 | self.target_stocks = [] 42 | self.dh_factory = DH_factory() 43 | @abstractmethod 44 | def start(self): 45 | 46 | #instantiate connections to the data handler and execution handler. 47 | st_dh_queue = queue.LifoQueue() 48 | st_eh_conn, eh_sh_conn = Pipe() 49 | self.set_eh_dh_conns(st_dh_queue, st_eh_conn) 50 | # Set queue in DH 51 | self.DataHandler.set_sh_queue(st_dh_queue) 52 | 53 | #Thread incoming data stream from the data handler. 54 | dh_stream_thread = Thread( 55 | target=self.DataHandler.start_streaming, args=([""],)) 56 | dh_listen_thread = Thread(target=self.test_dh_queue, args=()) 57 | dh_stream_thread.start() 58 | dh_listen_thread.start() 59 | 60 | searcher_thread = Thread(target=self.listen_for_searcher, args=()) 61 | searcher_thread.start() 62 | 63 | 64 | # Initialize any technical indicators needed from the Lib. 65 | # Start strategy, pop stock from target stocks when needed 66 | 67 | @abstractmethod 68 | def listen_for_searcher(self): 69 | while True: 70 | target_stock = self.strat_search_conn.recv() 71 | if target_stock not in self.target_stocks: 72 | self.target_stocks.append(target_stock) 73 | 74 | 75 | @abstractmethod 76 | def next(self): 77 | #Buy Conditional 78 | #random buy thing for example. 79 | if self.position.size == 0: 80 | size = int(self.broker.getcash() / 1+ self.position.size) 81 | self.buy(size=size) 82 | # Sell Conditional 83 | 84 | pass 85 | 86 | @abstractmethod 87 | def run_strat(self): 88 | #just start streaming for example. 89 | self.start() 90 | self.ticker = self.queue[0] 91 | self.pop_queue() 92 | 93 | for i in range(0, 5): # stays on one stock for 5 minutes, before switching to next stock in priority queue 94 | self.dh.start_streaming(self.ticker) 95 | previous_time = int(time.time() - 60) 96 | current_time = int(time.time()) 97 | df_price = self.df.get_bars(ticker, previous_time, current_time, "1Min", "2") 98 | 99 | #df_current = self.st_dh_queue.pop() 100 | #df_previous = self.st_dh_queue.pop() 101 | 102 | #previous_close = df_previous["close"] 103 | #previous_open = df_previous["open"] 104 | previous_close = df_price.iloc[1, 4] 105 | previous_open = df_price.iloc[1, 1] 106 | previous_candle = previous_close - previous_open 107 | 108 | #current_close = df.current["close"] 109 | #current_open = df.current["open"] 110 | current_close = df_price.iloc[0, 4] 111 | current_open = df_price.iloc[0, 1] 112 | current_candle = current_close - current_open 113 | 114 | # Bullish Engulfing Buy Condition 115 | if (previous_candle < 0 and current_candle > 0) and (current_open =< previous_open and current_close > previous_close): 116 | signal = 'buy' 117 | #self.eh.start_streaming(signal) 118 | #money_alloc = self.eh.money_alloc_pre(0.0025, 15) 119 | self.eh.create_order(self.ticker, 5, signal, 'market', 'gtc') 120 | 121 | # Bearish Engulfing Sell Condition 122 | if (previous_candle > 0 and current_candle < 0) and (current_open >= previous_open and current_close < previous_close): 123 | signal = 'sell' 124 | #self.eh.start_streaming(signal) 125 | self.eh.create_order(self.ticker, 5, signal, 'market', 'gtc') 126 | 127 | #time.sleep(60) 128 | #next_time = current_time + 60 129 | #next_time = round(current_time + 60, 0) 130 | next_time = current_time + 60 131 | while time.time() < next_time: 132 | #time.sleep(1) 133 | 134 | """ 135 | if time.time() = next_time: 136 | continue 137 | else: 138 | time.sleep(1) 139 | """ 140 | 141 | """ 142 | Overview: sets the pipe connections 143 | 144 | Requires: none 145 | Modifies: none 146 | Effects: none 147 | Returns: none 148 | Throws: RunTimeError if any of the parameters are null 149 | """ 150 | def set_eh_dh_conns(self, dh_q, eh_conn): 151 | if dh_q is None or eh_conn is None: 152 | raise RuntimeError('set_eh_dh_conns called with a null') from exc 153 | self.dh_queue = dh_q 154 | self.eh_conn = eh_conn 155 | """ 156 | Overview: adds a stock to the queue 157 | 158 | Requires: none 159 | Modifies: self.queue 160 | Effects: appends "stock" to self.queue 161 | Returns: none 162 | Throws: none 163 | """ 164 | def add_queue(self, stock): 165 | printf("Adding {} to queue".format(stock)) 166 | self.queue.append(stock) 167 | """ 168 | Overview: pops a stock from the queue 169 | 170 | Requires: queue is not empty 171 | Modifies: self.queue 172 | Effects: appends "stock" to self.queue 173 | Returns: none 174 | Throws: RuntimeError if queue is empty 175 | """ 176 | def pop_queue(self, pos=0): 177 | if len(self.queue) == 0: 178 | raise RuntimeError('cannot pop element from an empty queue') from exc 179 | else: 180 | printf("Popping {} from queue".format(self.queue[pos])) 181 | self.queue.pop(pos) -------------------------------------------------------------------------------- /client/src/style.css: -------------------------------------------------------------------------------- 1 | 2 | @import url('https://fonts.googleapis.com/css2?family=Titillium+Web:wght@300&display=swap'); 3 | 4 | body { 5 | margin: 0; 6 | /*font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 7 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 8 | sans-serif;*/ 9 | font-family: 'Titillium Web'; 10 | -webkit-font-smoothing: antialiased; 11 | -moz-osx-font-smoothing: grayscale; 12 | } 13 | 14 | input, button{ 15 | font-family: 'Titillium Web'; 16 | } 17 | 18 | code { 19 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 20 | monospace; 21 | } 22 | /*---------------------------------------------------------------------------------*/ 23 | /* General CSS */ 24 | /*---------------------------------------------------------------------------------*/ 25 | .center { 26 | justify-content: center; 27 | align-content: center; 28 | align-items: center; 29 | } 30 | 31 | .center-text { 32 | text-align: center; 33 | vertical-align: middle; 34 | } 35 | 36 | .page-wrapper { 37 | position: absolute; 38 | width: 100%; 39 | height: 100%; 40 | overflow: hidden; 41 | } 42 | 43 | /*---------------------------------------------------------------------------------*/ 44 | /* Login Page CSS */ 45 | /*---------------------------------------------------------------------------------*/ 46 | 47 | .login-page-wrapper { 48 | position: relative; 49 | width: 135%; 50 | height: 100%; 51 | display: flex; 52 | flex-flow: row; 53 | transition: all 0.3s ease; 54 | overflow-x: hidden; 55 | } 56 | 57 | .register-wrapper { 58 | width: 35%; 59 | height: 100%; 60 | background: #DE8B57; 61 | /*background: #;*/ 62 | } 63 | 64 | 65 | .login-content-wrapper { 66 | width: 65%; 67 | max-width: 60%; 68 | height: 100%; 69 | background: #ffffff; 70 | display: flex; 71 | flex-flow: column; 72 | } 73 | .login-wrapper { 74 | width: 35%; 75 | height: 100%; 76 | background: #669EC5; 77 | display: flex; 78 | flex-flow: column; 79 | /*background: #0089FA;*/ 80 | } 81 | 82 | .login-content-header { 83 | width: 100%; 84 | height: 100px; 85 | display: flex; 86 | padding-bottom: 12%; 87 | } 88 | 89 | .login-content-header img{ 90 | height: 40px; 91 | } 92 | 93 | .login-content-img-wrapper { 94 | width: 100%; 95 | height: auto; 96 | display: flex; 97 | padding-bottom: 20px; 98 | } 99 | 100 | .login-content-img-wrapper img { 101 | height: auto; /* 45vh */ 102 | } 103 | .login-content-text-wrapper { 104 | width: 80%; 105 | height: auto; 106 | display: flex; 107 | flex-flow: column; 108 | text-align: center; 109 | } 110 | 111 | .login-context-title { 112 | display: flex; 113 | /*color: #0089FA;*/ 114 | color: #669EC5; 115 | font-size: 24px; 116 | padding-bottom: 5px; 117 | } 118 | 119 | .login-context-subtitle { 120 | display: flex; 121 | flex: 1; 122 | 123 | font-size: 18px; 124 | } 125 | .login-content-footer { 126 | flex: 1; 127 | width: 100%; 128 | } 129 | 130 | .login-header{ 131 | width: 100%; 132 | height: 50px; 133 | display: flex; 134 | padding-bottom: 20%; 135 | justify-content: space-between; 136 | padding-top: 10px; 137 | } 138 | 139 | .login-header-btn { 140 | width: auto; 141 | height: 50px; 142 | display: flex; 143 | padding: 0px 10px 0px 10px; /*Top Right Bottom Left*/ 144 | } 145 | 146 | .login-header-btn button{ 147 | width: auto; 148 | height: 34px; 149 | display: flex; 150 | border-radius: 17px; 151 | border: solid 1.5px #ffffff; 152 | background: none; 153 | color: #ffffff; 154 | padding: 5px 15px 5px 15px; 155 | } 156 | 157 | .login-page-form-title-wrapper{ 158 | width: 100%; 159 | height: auto; 160 | display: flex; 161 | flex-flow: column; 162 | padding: 10px 0px 20px 0px; /*Top Right Bottom Left*/ 163 | } 164 | 165 | .login-page-form-title{ 166 | display: flex; 167 | /*color: #0089FA;*/ 168 | color: #ffffff; 169 | font-weight: bold; 170 | font-size: 24px; 171 | height: 50%; 172 | } 173 | 174 | .login-page-form-subtitle { 175 | display: flex; 176 | flex: 1; 177 | color: #ffffff; 178 | font-size: 18px; 179 | } 180 | 181 | .login-body{ 182 | width: 100%; 183 | height: auto; 184 | display: flex; 185 | flex-flow: column; 186 | } 187 | 188 | .login-footer{ 189 | flex: 1; 190 | } 191 | 192 | .login-page-input-wrapper { 193 | width: 100%; 194 | height: auto; 195 | display: flex; 196 | flex-flow: column; 197 | justify-content: center; 198 | align-items: center; 199 | padding-bottom: 20px; 200 | 201 | } 202 | 203 | .login-page-text-input-title { 204 | width: 66%; 205 | height: auto; 206 | color: #ffffff; 207 | 208 | } 209 | 210 | .login-page-text-input-wrapper { 211 | width: 66%; 212 | height: auto; 213 | display: flex; 214 | flex-flow: column; 215 | justify-content: center; 216 | } 217 | 218 | .login-page-text-input-wrapper input { 219 | width: 100%; 220 | height: 30px; 221 | border-radius: 5px; 222 | border: none; 223 | color: #ffffff; 224 | font-size: 16px; 225 | padding-left: 40px; 226 | } 227 | 228 | .login-page-text-input-wrapper input:focus { 229 | outline: none; 230 | 231 | } 232 | 233 | .login-submit-btn-wrapper { 234 | width: 100%; 235 | height: 80px; 236 | display: flex; 237 | 238 | } 239 | 240 | .login-submit-btn-wrapper button { 241 | width: 66%; 242 | height: 40px; 243 | border-radius: 20px; 244 | border: none; 245 | background: #DE8B57; 246 | color: #ffffff; 247 | font-size: 18px; 248 | } 249 | 250 | .register-submit-btn-wrapper { 251 | width: 100%; 252 | height: 80px; 253 | display: flex; 254 | 255 | } 256 | 257 | .register-submit-btn-wrapper button { 258 | width: 66%; 259 | height: 40px; 260 | border-radius: 20px; 261 | border: none; 262 | background: #669EC5; 263 | color: #ffffff; 264 | font-size: 18px; 265 | } 266 | 267 | .forgot-password { 268 | padding-top: 5px; 269 | padding-left: 40px ; 270 | color: #ffffff; 271 | width: 75%; 272 | text-align: right; 273 | } 274 | .forgot-password button { 275 | color: #ffffff; 276 | font-size: 14px; 277 | background: none; 278 | border: none; 279 | } 280 | .forgot-password button:hover { 281 | text-decoration: underline; 282 | } 283 | 284 | .testing { 285 | padding-left: 10px; 286 | position: absolute; 287 | padding-top: 6px; 288 | pointer-events: none; 289 | } 290 | 291 | .icons-wrapper { 292 | width: 100%; 293 | height: 50px; 294 | display: flex; 295 | padding-top: 40px; 296 | } 297 | .icons-container { 298 | width: 66%; 299 | height: auto; 300 | display: flex; 301 | flex-flow: row; 302 | justify-content: center; 303 | 304 | } 305 | .icons-container img { 306 | width: 60px; 307 | } -------------------------------------------------------------------------------- /src/BrokerBot.py: -------------------------------------------------------------------------------- 1 | # import DataHandler 2 | # import ExecutionHandler 3 | from PortfolioManager import * 4 | # from StrategyHandler import StrategyHandler 5 | # from DataHandler import AlpacaDataHandler 6 | from threading import Thread 7 | from queue import PriorityQueue 8 | from enum import Enum 9 | from ENUMS import * 10 | import time 11 | from multiprocessing import Process, Pipe 12 | import os 13 | from StrategyHandler import StrategyHandler 14 | 15 | """ 16 | overview: (description of the class) 17 | 18 | TODO: (to do of the class as a whole more long term things) 19 | """ 20 | 21 | 22 | class BrokerBot: 23 | 24 | """ 25 | Abstract Function: 26 | 27 | Representation Invariant: 28 | 29 | Simple explanation:(if nesscary) 30 | """ 31 | 32 | # ====================Creators==================== 33 | ''' 34 | Overview: constructs a BrokerBot instance. 35 | 36 | Requires: api_key: the api key for the api this instance uses. 37 | secret_key: the sectet key for the api this instance constructs 38 | base_url: the base url of the api. 39 | socket: the socket url for communication with the api 40 | Modifies: headers,market_open,api_key,secret_key,base_url,socket, 41 | account_url,order_url,strategy_handler_processes. 42 | Effects: headers is inilized and contains api_key, and secret_key 43 | market_open = true 44 | api_key stores the api key 45 | base_url stores the api's base url 46 | socket contian the api's socket port. 47 | account url stores a formatted url for the account(alpaca only) 48 | order url stores a formatted url for the orders(alpaca only) 49 | strategy_handler_processes is inilized to an empty list 50 | Returns: volume of the stock that was passed in 51 | Throws: RunTimeException if any parameter is null. 52 | TODO: How many tickers are we limited to per API request? Answer: 200 53 | sockets limited to 30 54 | ''' 55 | 56 | #def __init__(self, api_key, secret_key, base_url, socket, search_conn): 57 | def __init__(self, api_key, secret_key, base_url, socket, search_conn): 58 | if api_key is None or secret_key is None or base_url is None or socket is None: 59 | raise RuntimeError('BrokerBot initalized with a null') from exc 60 | 61 | self.market_open = True 62 | self.api_key = api_key 63 | self.secret_key = secret_key 64 | self.base_url = base_url 65 | self.socket = socket 66 | 67 | self.headers = {} 68 | self.account_url = "" 69 | self.order_url = "" 70 | 71 | self.pm = PortfolioManager(api_key, secret_key, base_url, socket) 72 | self.input = self.pm.input 73 | self.set_vars() 74 | 75 | #self.searcher_conn = search_conn 76 | self.sh_pipe_conns = [] 77 | self.sh_instances = [] 78 | self.sh_processes = [] 79 | # ====================Observers==================== 80 | 81 | ''' 82 | Overview: Updates handlers based on portfoliomanager values 83 | Requires: none 84 | Modifies: none 85 | Effects: none 86 | Returns: none 87 | Throws: none 88 | TODO: Add checking for if strategy or risk change and update 89 | handlers accordingly 90 | ''' 91 | def update(self): 92 | if(self.pm.input != self.input): 93 | self.input = self.pm.input 94 | pass 95 | ''' 96 | Overview: returns the account 97 | 98 | Requires: none 99 | Modifies: none 100 | Effects: none 101 | Returns: a joson containing the contents of the account. 102 | Throws: ??? 103 | TODO: figure out what this Might throw 104 | ''' 105 | 106 | def get_account(self): 107 | r = requests.get(self.account_url['alpaca'], headers['alpaca']) 108 | return json.loads(r.content) 109 | # ====================Producers==================== 110 | # ====================Mutators==================== 111 | ''' 112 | Overview: sets market_open to false 113 | 114 | Requires: none 115 | Modifies: market_open 116 | Effects: market_open set to false 117 | Returns: none 118 | Throws: none 119 | TODO: 120 | ''' 121 | 122 | def set_market_close(self): 123 | self.market_open = False 124 | ''' 125 | Overview: sets variables based on portfolio manager inputs 126 | 127 | Requires: none 128 | Modifies: self.headers, self.account_url, self.order_url 129 | Effects: All three values updated based on PortfolioManager 130 | values of them 131 | Returns: none 132 | Throws: none 133 | TODO: 134 | ''' 135 | def set_vars(self): 136 | self.headers = self.pm.headers 137 | self.account_url = self.pm.account_url 138 | self.order_url = self.pm.order_url 139 | ''' 140 | Overview: infinite loop to update portfoliomanager values 141 | 142 | Requires: none 143 | Modifies: self.input 144 | Effects: self.input updates based on user input 145 | Returns: none 146 | Throws: none 147 | TODO: 148 | ''' 149 | def get_commands(self): 150 | return { 151 | "changestrat": self.pm.change_strat, 152 | "changerisk": self.pm.change_risk, 153 | "getstrat": self.pm.get_strat, 154 | "totalreturn": self.pm.get_total_return, 155 | "todaysreturn": self.pm.get_todays_return, 156 | "deposit": self.pm.deposit, 157 | "withdraw": self.pm.withdraw, 158 | "balance": self.pm.get_balance, 159 | "liquidbalance": self.pm.get_current_liquid_cash, 160 | "getcurrstrat": self.pm.get_current_strat, 161 | "orderhistory": self.pm.order_history, 162 | "checkpositions": self.pm.check_positions 163 | } 164 | 165 | 166 | def listen_for_searcher(self): 167 | while True: 168 | target_stocks = self.searcher_conn.recv() 169 | for sh_conn in self.sh_pipe_conns: 170 | sh_conn.send(target_stocks) 171 | 172 | 173 | ''' 174 | Overview: Start SH on own process via multiprocessing 175 | 176 | Requires: none 177 | Modifies: none 178 | Effects: none 179 | Returns: none 180 | Throws: RuntimeError on execution 181 | 182 | TODO: Specfification & figure out strategy logic/pipeline 183 | ''' 184 | 185 | def run(self): 186 | # strategies = ["ST1", "ST2", "ST3"] 187 | strategies = ["ST1"] 188 | 189 | for strat in strategies: 190 | bb_sh_conn, sh_bb_conn = Pipe() 191 | self.sh_pipe_conns.append(bb_sh_conn) 192 | self.sh_instances.append(StrategyHandler( 193 | self.api_key, self.secret_key, self.base_url, self.socket, strat, self.input)) 194 | 195 | for sh in self.sh_instances: 196 | self.sh_processes.append(Process(target=sh.run, args=())) 197 | 198 | for proc in self.sh_processes: 199 | proc.start() 200 | 201 | while True: 202 | if not self.market_open: 203 | for proc in sh_processes: 204 | proc.join() 205 | else: 206 | commands = self.get_commands() 207 | cmd_list = list(commands) 208 | 209 | user = input() 210 | if(user == 'q'): 211 | break 212 | if(user in cmd_list): 213 | commands.get(user)() 214 | pass 215 | else: 216 | print(f"Invalid Input - command options are {cmd_list}") 217 | self.update() 218 | -------------------------------------------------------------------------------- /src/StrategyHandler.py: -------------------------------------------------------------------------------- 1 | from DataHandler import AlpacaDataHandler 2 | from ExecutionHandler import AlpacaExecutionHandler 3 | from threading import Thread 4 | import queue 5 | import copy 6 | from multiprocessing import Process, Pipe 7 | 8 | 9 | """ 10 | Overview: 11 | class that handles running our different strategies, using DH & EH's that are 12 | customized to what ever api constructed with 13 | TODO: 14 | construct proper DH, and EH for whatever APi selected 15 | select from avaliable DH's ex binance DH, alpaca DH, .. ect. 16 | decide what fucntion/ stratey its running 17 | ex. longterm, short term, medium term... ect. 18 | instanciate member variables 19 | log(Data structure communicating with the DH) 20 | DH 21 | EH 22 | """ 23 | 24 | 25 | class StrategyHandler: 26 | # ====================Creators==================== 27 | """ 28 | Overview: High-Risk Strategy will be implemented here. Below is just an example to give an idea. 29 | 30 | Requires: api_key,secret_key,base_url,data_url,socket, base_url, ticker, 31 | DH_API,EH_API, 32 | 33 | Modifies: DH, EH,log, 34 | Effects: DH is initialized, EH is initialized, stream is listend to, Log is initialized. 35 | Returns: none 36 | """ 37 | 38 | def __init__(self, api_key, secret_key, base_url, socket, strategy, strat_input): 39 | self.api_key = api_key 40 | self.secret_key = secret_key 41 | self.base_url = base_url 42 | self.socket = socket 43 | self.strategy_category = strategy 44 | self.strategy_input = strat_input 45 | 46 | self.bb_conn = bb_conn 47 | self.dh_queue = None 48 | self.eh_conn = None 49 | self.target_stocks = [] 50 | # ====================Observers==================== 51 | """ 52 | Overview: tests & prints if we revieved a message from the DH 53 | 54 | Requires: none 55 | Modifies: none 56 | Effects: none 57 | Returns: none 58 | Throws: none 59 | """ 60 | 61 | def test_dh_queue(self): 62 | while True: 63 | data_frame = self.dh_queue.get() 64 | print(f"SH RECV: {data_frame}") 65 | # ====================Producers==================== 66 | # ====================Mutators==================== 67 | 68 | """ 69 | Example Trading strategy 70 | def shortTerm(ticker): 71 | listen to stream of ticker\ 72 | start adding to the message log 73 | while(true): 74 | add to the message log 75 | stat calculations for the strategy 76 | calculate trade signal 77 | EH.enterTrade(ticker, signal) 78 | while(order has not been fullfilled){ 79 | // check if order fullfuill has been recieved 80 | // add to log 81 | //continue contiuous calculations 82 | } 83 | begin dynamic stop loss caluclations, by continuing the technical indicator calculations 84 | stoploss(); 85 | """ 86 | 87 | """ 88 | Overview: High-Risk Strategy will be implemented here. Below is just an example to give an idea. 89 | 90 | Requires: bars is non-null 91 | Modifies: none 92 | Effects: none 93 | Returns: sell-trade decision if previous close is less than fibonacci value AND current open is less than fibonacci value, else returns none representing no decision is made 94 | """ 95 | def HighRisk(symbol, bars): 96 | fib_values = self.fibonacci(symbol, bars) 97 | # elif current_price == fib_values[] 98 | for fib_val in fib_values: 99 | if previous_close < fib_val and current_open < fib_val: 100 | # by returning 'SHORT', this will tell execution handler to make a short trade 101 | decision = ["SHORT", None, None] 102 | return decision 103 | return None 104 | 105 | """ 106 | Overview: Medium-Risk Strategy will be implemented here. Below is just an example to give an idea. 107 | 108 | Requires: bars is non-null 109 | Modifies: none 110 | Effects: none 111 | Returns: buy-trade decision, take-profit, and stop-loss if current volume is 1.5x greater than previous volume. 112 | """ 113 | def MidRisk(bars): 114 | pass 115 | 116 | """ 117 | Overview: Low-Risk Strategy will be implemented here. Below is just an example to give an idea. 118 | 119 | Requires: bars is non-null 120 | Modifies: none 121 | Effects: none 122 | Returns: buy-trade decision, take-profit, and stop-loss if current volume is 1.5x greater than previous volume. 123 | """ 124 | def LowRisk(symbol, bars): 125 | vol_1 = self.volume(bars[0]) 126 | # I think each bar should be tuple with open, close, low, high, current prices & volume 127 | vol_2 = self.volume(bars[1]) 128 | 129 | if vol_1 > (vol_2 * 1.5): 130 | fib_values = self.fibonacci(symbol, bars) 131 | TP = fib_values[3] # take profit is the 3rd retracement 132 | SL = fib_values[2] # stop loss is the 2nd retracement 133 | decision = ["BUY", TP, SL] 134 | return decision 135 | 136 | """ 137 | Overview: calculates the fibonacci values of 23.6%, 38.2%, 50%, 61.8%, and 78.6% 138 | 139 | Requires: bars is non-null 140 | Modifies: none 141 | Effects: none 142 | Returns: fibonacci values 143 | Throws: RunTimeException if bars or symbol is null 144 | """ 145 | def fibonacci(symbol, bars): 146 | if symbol is None or bars is None: 147 | raise RuntimeError('fibonacci called with a null') from exc 148 | # first data-value in the bars array (most recent bar to current bar) 149 | first = bars[0] 150 | # last data-value in the bars array (most farthest bar to current bar) 151 | second = bars[len(candles) - 1] 152 | retracements = [0.236, 0.382, 0.5, 0.618, 0.786] 153 | fib_values = [(second - ((second - first) * retracement)) 154 | for retracement in retracements] 155 | return fib_values 156 | """ 157 | Overview: sets the pipe connections 158 | 159 | Requires: none 160 | Modifies: none 161 | Effects: none 162 | Returns: none 163 | Throws: RunTimeError if any of the parameters are null 164 | """ 165 | 166 | def set_eh_dh_conns(self, dh_q, eh_conn): 167 | if dh_q is None or eh_conn is None: 168 | raise RuntimeError('set_eh_dh_conns called with a null') from exc 169 | self.dh_queue = dh_q 170 | self.eh_conn = eh_conn 171 | 172 | 173 | """ 174 | Overview: Create DH + EH process and pipe connection points for both 175 | 176 | Requires: none 177 | Modifies: DataHandler, 178 | Effects: DataHandeler starts a stream on ticket "TSLA" 179 | Returns: none 180 | Throws: none 181 | TODO: figure out best way to pass these pipe connections points to DH and EH 182 | TODO: add logic in SH and EH for using pipe to communication with SH 183 | """ 184 | 185 | def run(self): 186 | # TODO: add logic for starting proper strategies from a given categroy 187 | sample_strategies = ["strat1", "strat2", "strat3"] 188 | searcher_strat_conns = [] 189 | # add switch statement for instantiating correct strategy class based on strat ^ 190 | for start in sample_strategies: 191 | dh_params = [self.api_key, self.secret_key, self.base_url, self.socket] 192 | st_dh = self.dh_factory.construct_dh(self.strategy_input[2], dh_params) 193 | eh_params = [self.api_key, self.secret_key, self.base_url] 194 | st_eh = self.eh_factory.construct_eh(self.strategy_input[3], eh_params) 195 | search_strat, strat_search = Pipe() 196 | 197 | # replace with real class 198 | self.strategies.append(Strategy(st_dh, st_eh, "None", strat_search)) 199 | searcher_strat_conns.append(search_strat) 200 | 201 | self.searcher = Searcher(self.api_key, self.secret_key, self.base_url, self.socket, searcher_strat_conns) 202 | 203 | searcher_proc = Process(target=self.searcher.search, args=()) 204 | searcher_proc.start() 205 | 206 | strategy_processes = [] 207 | for strat in self.strategies: 208 | strat_proc = Process(target=strat.start, args=()) 209 | strat_proc.start() 210 | strategy_processes.append(strat_proc) 211 | 212 | 213 | dh_listen_thread = Thread(target=self.test_dh_queue, args=()) 214 | 215 | dh_stream_thread.start() 216 | dh_listen_thread.start() 217 | 218 | 219 | 220 | prev_target_stocks = copy.deepcopy(self.target_stocks) 221 | # TODO: create logic for determining channel name based on strat 222 | channel_name = "T" 223 | # TODO: refine this once searcher routine more explictily defined 224 | while True: 225 | # if BB updates us with new target stocks from searcher, clean up DH process and start new one with updated target stocks 226 | new_target_stocks = self.bb_conn.recv() 227 | target_tickers = set(prev_target_stocks + new_target_stocks) 228 | self.DataHandler.listen(target_tickers, channel_name) 229 | 230 | 231 | -------------------------------------------------------------------------------- /src/DataHandler.py: -------------------------------------------------------------------------------- 1 | import websocket 2 | import requests 3 | import json # For web connectivity 4 | from abc import ABC, abstractmethod # Abstract class module for python. 5 | import alpaca_trade_api as tradeapi 6 | from dataclasses import dataclass # Python structs module. 7 | import pandas as pd # For data storage and analysis. 8 | import ast, datetime # For on_message data handling 9 | """ 10 | overview: 11 | - DataHandler Class: DataHandler is a class that takes in data from a given brokerage API, 12 | and sends it through to the StrategyHandler in a format that it can natively understand. 13 | in short, it is responsible for data transfer and formatting. 14 | 15 | - Standard Data Format (BBFrame): BBFrame is the standard data format used in the BrokerBot 16 | project. it consists of a Pandas dataframe in the following configuration: 17 | ================================================================ 18 | | | Time | Open | High | Low | Close | Volume | <- Labelled columns 19 | | 01 | string | float | float | float | float | float | 20 | | 02 | string | float | float | float | float | float | 21 | ================================================================ 22 | ^^ 23 | auto-numbered rows (see pandas Dataframe for more info.) 24 | 25 | TODO: 26 | - More concretely define abstract methods for base class. 27 | - Support more API subclasses. 28 | - 29 | """ 30 | 31 | 32 | class DataHandler(ABC): 33 | # ==================== Creators ==================== 34 | 35 | # ==================== Observers =================== 36 | """ 37 | requires: nothing. 38 | modifies: nothing. 39 | effects: nothing. 40 | returns: an account object for the API. 41 | """ 42 | @abstractmethod 43 | def get_account(self): 44 | pass 45 | """ 46 | requires: nothing. 47 | modifies: nothing. 48 | effects: nothing. 49 | returns: the socket object for the API. 50 | """ 51 | @abstractmethod 52 | def get_socket(self): 53 | pass 54 | 55 | # ==================== Producers =================== 56 | 57 | """ 58 | requires: ticker for given stock, start time for bar data, end time of bar data, and length of bar. 59 | modifies: nothing. 60 | effects: nothing. 61 | returns: a Pandas Dataframe containing the bars data in BrokerBot Standard Format (BBFrame). 62 | """ 63 | @abstractmethod 64 | def get_bars(self, tickers, bar_timeframe, num_of_bars): 65 | pass 66 | 67 | 68 | # ==================== Mutators ==================== 69 | """ 70 | requires: The new account to set this one to. 71 | modifies: The current account object, replacing it with the argument account. 72 | effects: nothing. 73 | returns: nothing. 74 | """ 75 | @abstractmethod 76 | def set_account(self, account): 77 | pass 78 | 79 | 80 | # ===================== Misc ======================= 81 | 82 | @abstractmethod 83 | def on_open(self): 84 | pass 85 | 86 | @abstractmethod 87 | def on_message(self): 88 | pass 89 | 90 | @abstractmethod 91 | def on_close(self): 92 | pass 93 | 94 | @abstractmethod 95 | def on_error(self): 96 | pass 97 | 98 | @abstractmethod 99 | def listen(self): 100 | pass 101 | 102 | @abstractmethod 103 | def unlisten(self): 104 | pass 105 | 106 | 107 | class AlpacaDataHandler(DataHandler): 108 | def __init__(self, 109 | api_key, 110 | secret_key, 111 | base_url, 112 | socket="ws://data.alpaca.markets/stream"): 113 | self.api_key = api_key 114 | self.secret_key = secret_key 115 | self.headers = { 116 | "APCA-API-KEY-ID": api_key, 117 | "APCA-API-SECRET-KEY": secret_key 118 | } 119 | self.base_url = base_url 120 | self.account_url = "{}/v2/account".format(self.base_url) 121 | self.order_url = "{}/v2/orders".format(self.base_url) 122 | self.api = tradeapi.REST(self.headers["APCA-API-KEY-ID"], 123 | self.headers["APCA-API-SECRET-KEY"], base_url) 124 | self.api_account = self.api.get_account() 125 | self.ws = None 126 | self.socket = socket 127 | self.pending_tickers = [] 128 | self.sh_queue = None 129 | 130 | def set_sh_queue(self, q): 131 | self.sh_queue = q 132 | 133 | def get_account(self): 134 | return self.api_account 135 | 136 | def set_account(self, account): 137 | self.acount = account 138 | 139 | def get_socket(self): 140 | return self.ws 141 | 142 | 143 | """ 144 | requires: Strings for the ticker, start and end time in RFC-3339 format(e.g. 2021-03-11T00:00:00-05:00), 145 | timeframe (currently only '1Day', '1Hour', and '1Min'), 146 | and bar_limit, which limits the number of bars returned within that timeframe. 147 | modifies: nothing. 148 | effects: nothing. 149 | returns: A Pandas Dataframe containing the bars data. 150 | """ 151 | def get_bars(self, ticker: str, start_time: str, end_time: str, bar_timeframe: str, bar_limit: str): 152 | url ='https://data.alpaca.markets/v2/stocks'+'/'+ticker+'/bars?adjustment=raw'+'&start='+start_time+'&end='+end_time+'&limit='+bar_limit+'&page_token='+'&timeframe='+bar_timeframe 153 | r = requests.get(url, headers=self.headers) 154 | df = pd.read_json(json.dumps(r.json()['bars'])) 155 | df['oi'] = -1 156 | df.columns = ['Date', 'Open', 'High', 'Low', 'Close', 'Volume', 'Open Interest'] 157 | print(df) 158 | return df 159 | 160 | # Socket Functions 161 | 162 | def on_open(self, ws): 163 | print("on open") 164 | 165 | # function called whenever a websocket is opened, authenticates with alpaca 166 | 167 | auth_data = { 168 | "action": "authenticate", 169 | "data": { 170 | "key_id": self.api_key, 171 | "secret_key": self.secret_key 172 | } 173 | } 174 | ws.send(json.dumps(auth_data)) 175 | print("sent auth") 176 | listen_message = { 177 | "action": "listen", 178 | "data": { 179 | "streams": [f"AM.{self.pending_tickers.pop()}", "AM.GME"] 180 | } 181 | } 182 | # check pending tickers, sne initial listen message, wait for new tickers, 183 | ws.send(json.dumps(listen_message)) 184 | 185 | """ 186 | requires: Reference to the WebSocketApp and the message that was receieved. 187 | modifies: nothing. 188 | effects: Sends a DataFrame to SH via the pipe. 189 | returns: nothing. 190 | """ 191 | 192 | def on_message(self, ws, message): 193 | print("received a message") 194 | print(message) 195 | # convert message to dictionary 196 | message = ast.literal_eval(message) 197 | # message is not an authorization or listening message, so it must be a minute bars message 198 | if message["stream"] != "authorization" and message["stream"] != "listening": 199 | timestamp = datetime.datetime.fromtimestamp(message["data"]["e"] / 1000) 200 | timestamp = timestamp.isoformat("T") 201 | op = message["data"]["o"] 202 | high = message["data"]["h"] 203 | low = message["data"]["l"] 204 | cl = message["data"]["c"] 205 | vol = message["data"]["v"] 206 | data = [[timestamp, op, high, low, cl, vol]] 207 | df = pd.DataFrame(data, columns=["Time", "Open", "High", "Low", "Close", "Volume"]) 208 | self.sh_queue.put(df) 209 | 210 | def on_close(self, ws): 211 | print("closed connection") 212 | 213 | def on_error(self, ws, error): 214 | print(error) 215 | 216 | def start_streaming(self, tickers): 217 | self.pending_tickers += tickers 218 | print(self.socket) 219 | self.ws = websocket.WebSocketApp( 220 | self.socket, 221 | on_message=lambda ws, msg: self.on_message(self.ws, msg), 222 | on_close=lambda ws: self.on_close(self.ws), 223 | on_open=lambda ws: self.on_open(self.ws), 224 | on_error=lambda ws, error: self.on_error(self.ws, error)) 225 | 226 | self.ws.run_forever() 227 | print("HELLO") 228 | 229 | def listen(self, tickers, channel_name): 230 | # 231 | # function that sends a listen message to alpaca for the streams inputed. 232 | # 233 | for x in range(len(tickers)): 234 | tickers[x] = channel_name + "." + tickers[x] 235 | print(tickers) 236 | listen_message = {"action": "listen", "data": {"streams": tickers}} 237 | self.ws.send(json.dumps(listen_message)) 238 | #self.stream_conn.run(quote_callback, tickers) 239 | 240 | def unlisten(self, ticker, channel_name): 241 | # 242 | # function that unlistens for the streams inputed. 243 | # might need error checking if a stream that is not currently being listened to is asked to be unlistened. 244 | # 245 | for x in range(ticker): 246 | ticker[x] = channel_name + ticker[x] 247 | unlisten_message = {"action": "unlisten", "data": {"streams": ticker}} 248 | self.ws.send(json.dumps(unlisten_message)) 249 | -------------------------------------------------------------------------------- /client/src/Components/Pages/images/stocks.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/Factory.py: -------------------------------------------------------------------------------- 1 | from DataHandler import DataHandler 2 | from ExecutionHandler import ExecutionHandler 3 | from Strategy import Strategy 4 | 5 | class DH_factory: 6 | 7 | # Overview: Class that creates and returns any DH object 8 | 9 | """ 10 | Overview: Constructs and returns proper DH based on passed in enum 11 | 12 | Params: ENUM for DH_api 13 | params is list containg DH parameters 14 | Requires: none 15 | Modifies: none 16 | Effects: none 17 | Returns: Valid DH object based on parameter 18 | Throws: ValueError if parameter is invalid 19 | """ 20 | def construct_dh(self, enum, params): 21 | if enum == 1: 22 | return self.dh_alpaca(params) 23 | elif enum == 2: 24 | return self.dh_binance(params) 25 | elif enum == 3: 26 | return self.dh_polygon(params) 27 | elif enum == 4: 28 | return self.dh_ibkr(params) 29 | elif enum == 5: 30 | return self.dh_alpha(params) 31 | else: 32 | raise ValueError("Invalid ENUM") 33 | 34 | """ 35 | Overview: Constructs and returns alpaca DH based on params 36 | 37 | Params: params is a list of parameters for the alpaca api DH 38 | Requires: none 39 | Modifies: none 40 | Effects: none 41 | Returns: Valid alpaca DH object 42 | Throws: none 43 | """ 44 | def dh_alpaca(self, params): 45 | dh = AlpacaDataHandler(params[0], params[1], params[2], params[3]) 46 | return dh 47 | 48 | """ 49 | Overview: Constructs and returns binance DH based on params 50 | 51 | Params: params is a list of parameters for the binance api DH 52 | Requires: none 53 | Modifies: none 54 | Effects: none 55 | Returns: Valid binance DH object 56 | Throws: none 57 | """ 58 | def dh_binance(self, params): 59 | pass 60 | 61 | """ 62 | Overview: Constructs and returns polygon DH based on params 63 | 64 | Params: params is a list of parameters for the polygon api DH 65 | Requires: none 66 | Modifies: none 67 | Effects: none 68 | Returns: Valid polygon DH object 69 | Throws: none 70 | """ 71 | def dh_polygon(self, params): 72 | pass 73 | 74 | """ 75 | Overview: Constructs and returns ibkr DH based on params 76 | 77 | Params: params is a list of parameters for the ibkr api DH 78 | Requires: none 79 | Modifies: none 80 | Effects: none 81 | Returns: Valid ibkr DH object 82 | Throws: none 83 | """ 84 | def dh_ibkr(self, params): 85 | pass 86 | 87 | """ 88 | Overview: Constructs and returns alpha DH based on params 89 | 90 | Params: params is a list of parameters for the alpha api DH 91 | Requires: none 92 | Modifies: none 93 | Effects: none 94 | Returns: Valid alpha DH object 95 | Throws: none 96 | """ 97 | def dh_alpha(self, params): 98 | pass 99 | 100 | class EH_factory: 101 | 102 | # Overview: Class that creates and returns any EH object 103 | 104 | """ 105 | Overview: Constructs and returns proper DH based on passed in enum 106 | 107 | Params: ENUM for EH_api 108 | params is list containg EH parameters 109 | Requires: none 110 | Modifies: none 111 | Effects: none 112 | Returns: Valid EH object based on parameter 113 | Throws: ValueError if parameter is invalid 114 | """ 115 | def construct_eh(self, enum, params): 116 | if enum == 1: 117 | return self.eh_alpaca(params) 118 | elif enum == 2: 119 | return self.eh_binance(params) 120 | elif enum == 3: 121 | return self.eh_ibkr(params) 122 | elif enum == 4: 123 | return self.eh_alpha(params) 124 | else: 125 | raise ValueError("Invalid ENUM") 126 | 127 | """ 128 | Overview: Constructs and returns alpaca EH based on params 129 | 130 | Params: params is a list of parameters for the alpaca api DH 131 | Requires: none 132 | Modifies: none 133 | Effects: none 134 | Returns: Valid alpaca DH object 135 | Throws: none 136 | """ 137 | def eh_alpaca(self, params): 138 | eh = AlpacaExecutionHandler(params[0], params[1], params[2]) 139 | return eh 140 | 141 | """ 142 | Overview: Constructs and returns binance EH based on params 143 | 144 | Params: params is a list of parameters for the binance api EH 145 | Requires: none 146 | Modifies: none 147 | Effects: none 148 | Returns: Valid binance EH object 149 | Throws: none 150 | """ 151 | def eh_binance(self, params): 152 | pass 153 | 154 | 155 | """ 156 | Overview: Constructs and returns ibkr EH based on params 157 | 158 | Params: params is a list of parameters for the ibkr api EH 159 | Requires: none 160 | Modifies: none 161 | Effects: none 162 | Returns: Valid ibkr EH object 163 | Throws: none 164 | """ 165 | def eh_ibkr(self, params): 166 | pass 167 | 168 | """ 169 | Overview: Constructs and returns alpha EH based on params 170 | 171 | Params: params is a list of parameters for the alpha api EH 172 | Requires: none 173 | Modifies: none 174 | Effects: none 175 | Returns: Valid alpha EH object 176 | Throws: none 177 | """ 178 | def eh_alpha(self, params): 179 | pass 180 | 181 | class Strategy_factory: 182 | 183 | # Overview: Class that creates and returns any Strategy object 184 | 185 | """ 186 | Overview: Constructs and returns proper startegy based on passed in enum 187 | 188 | Params: ENUM for DH_api 189 | params is list containg DH parameters 190 | Requires: none 191 | Modifies: none 192 | Effects: none 193 | Returns: Valid DH object based on parameter 194 | Throws: ValueError if parameter is invalid 195 | """ 196 | def construct_strat(self, enum, params): 197 | if enum == 1: 198 | return self.short_low_risk(params) 199 | elif enum == 2: 200 | return self.medium_low_risk(params) 201 | elif enum == 3: 202 | return self.long_low_risk(params) 203 | elif enum == 4: 204 | return self.medium_low_risk(params) 205 | elif enum == 5: 206 | return self.medium_medium_risk(params) 207 | elif enum == 6: 208 | return self.long_medium_risk(params) 209 | elif enum == 7: 210 | return self.short_high_risk(params) 211 | elif enum == 8: 212 | return self.medium_high_risk(params) 213 | elif enum == 9: 214 | return self.long_high_risk(params) 215 | else: 216 | raise ValueError("Invalid ENUM") 217 | 218 | """ 219 | Overview: Constructs and returns short_low_risk strat based on parameters 220 | 221 | Params: params is a list of parameters for the strategy 222 | Requires: none 223 | Modifies: none 224 | Effects: none 225 | Returns: Valid short_low_risk strategy object 226 | Throws: none 227 | """ 228 | def short_low_risk(self, params): 229 | strat = Strategy(0,params[0], params[1], params[2], params[3]) 230 | return strat 231 | 232 | """ 233 | Overview: Constructs and returns medium_low_risk strat based on parameters 234 | 235 | Params: params is a list of parameters for the strategy 236 | Requires: none 237 | Modifies: none 238 | Effects: none 239 | Returns: Valid medium_low_risk strategy object 240 | Throws: none 241 | """ 242 | def medium_low_risk(self, params): 243 | strat = Strategy(1,params[0], params[1], params[2], params[3]) 244 | return strat 245 | 246 | """ 247 | Overview: Constructs and returns long_low_risk strat based on parameters 248 | 249 | Params: params is a list of parameters for the strategy 250 | Requires: none 251 | Modifies: none 252 | Effects: none 253 | Returns: Valid long_low_risk strategy object 254 | Throws: none 255 | """ 256 | def long_low_risk(self, params): 257 | strat = Strategy(2,params[0], params[1], params[2], params[3]) 258 | return strat 259 | 260 | """ 261 | Overview: Constructs and returns short_medium_risk strat based on parameters 262 | 263 | Params: params is a list of parameters for the strategy 264 | Requires: none 265 | Modifies: none 266 | Effects: none 267 | Returns: Valid short_medium_risk strategy object 268 | Throws: none 269 | """ 270 | def short_medium_risk(self, params): 271 | strat = Strategy(3,params[0], params[1], params[2], params[3]) 272 | return strat 273 | 274 | """ 275 | Overview: Constructs and returns medium_medium_risk strat based on parameters 276 | 277 | Params: params is a list of parameters for the strategy 278 | Requires: none 279 | Modifies: none 280 | Effects: none 281 | Returns: Valid medium_medium_risk strategy object 282 | Throws: none 283 | """ 284 | def medium_medium_risk(self, params): 285 | strat = Strategy(4,params[0], params[1], params[2], params[3]) 286 | return strat 287 | 288 | """ 289 | Overview: Constructs and returns long_medium_risk strat based on parameters 290 | 291 | Params: params is a list of parameters for the strategy 292 | Requires: none 293 | Modifies: none 294 | Effects: none 295 | Returns: Valid long_medium_risk strategy object 296 | Throws: none 297 | """ 298 | def long_medium_risk(self, params): 299 | strat = Strategy(5,params[0], params[1], params[2], params[3]) 300 | return strat 301 | 302 | """ 303 | Overview: Constructs and returns short_high_risk strat based on parameters 304 | 305 | Params: params is a list of parameters for the strategy 306 | Requires: none 307 | Modifies: none 308 | Effects: none 309 | Returns: Valid short_high_risk strategy object 310 | Throws: none 311 | """ 312 | def short_high_risk(self, params): 313 | strat = Strategy(6,params[0], params[1], params[2], params[3]) 314 | return strat 315 | 316 | """ 317 | Overview: Constructs and returns medium_high_risk strat based on parameters 318 | 319 | Params: params is a list of parameters for the strategy 320 | Requires: none 321 | Modifies: none 322 | Effects: none 323 | Returns: Valid medium_high_risk strategy object 324 | Throws: none 325 | """ 326 | def medium_high_risk(self, params): 327 | strat = Strategy(7,params[0], params[1], params[2], params[3]) 328 | return strat 329 | 330 | """ 331 | Overview: Constructs and returns long_high_risk strat based on parameters 332 | 333 | Params: params is a list of parameters for the strategy 334 | Requires: none 335 | Modifies: none 336 | Effects: none 337 | Returns: Valid long_high_risk strategy object 338 | Throws: none 339 | """ 340 | def long_high_risk(self, params): 341 | strat = Strategy(8,params[0], params[1], params[2], params[3]) 342 | return strat -------------------------------------------------------------------------------- /src/PortfolioManager.py: -------------------------------------------------------------------------------- 1 | #Imports 2 | # from StrategyHandler import StrategyHandler 3 | # from DataHandler import AlpacaDataHandler 4 | # from threading import Thread 5 | # from queue import PriorityQueue 6 | from ENUMS import * 7 | import alpaca_trade_api as tradeapi 8 | import time 9 | import matplotlib.pyplot as plt 10 | import sys 11 | import itertools 12 | import requests 13 | import json 14 | import pandas as pd 15 | 16 | class PortfolioManager: 17 | 18 | # Overview: PortfolioManager acts as a basic UI for BrokerBot. 19 | # Allows for a user to input commands and outputs basic 20 | # information about BrokerBot operations 21 | # To Do: Create Constructor. I/O for api's, strategies, stop/loss, cap 22 | # Be able to interface with other classes 23 | 24 | # ====================Creators==================== 25 | def __init__(self, api_key, secret_key, base_url, socket): 26 | self.api_key = api_key 27 | self.secret_key = secret_key 28 | self.base_url = base_url 29 | self.socket = socket 30 | 31 | self.headers = {} 32 | self.api = {} 33 | self.account_url = "" 34 | self.order_url = "" 35 | self.input = [] 36 | 37 | self.balance = 0 38 | self.liquid = 0 39 | self.assets = 0 40 | self.active = 0 41 | self.watch_list = "" 42 | self.combos = self.calc_combos() 43 | self.strategies = [] 44 | 45 | # inputs in the form [strategy, risk, DH_api, EH_api, stop_loss] 46 | self.initial_setup() 47 | 48 | # ====================Observers==================== 49 | # Overview: Function to calculate all combinations of strategies 50 | # 51 | # Requires: none 52 | # Modifies: none 53 | # Effects: none 54 | # Returns: combinations calculated 55 | # Throws: none 56 | def calc_combos(self): 57 | strats = [1,2,3,4,5,6,7,8,9] 58 | risks = [1,2,3,4,5,6,7,8,9] 59 | dh_api = [1,2,3,4,5] 60 | eh_api = [1,2,3,4] 61 | stop_loss = [1,2] 62 | combined = [strats,risks,dh_api,eh_api,stop_loss] 63 | combos = list(itertools.product(*combined)) 64 | return combos 65 | 66 | # Overview: Function to return self.input values 67 | # 68 | # Requires: none 69 | # Modifies: none 70 | # Effects: none 71 | # Returns: self.input 72 | # Throws: none 73 | def get_input(self): 74 | return self.input 75 | 76 | # Overview: Function to return all combinations of strategies 77 | # 78 | # Requires: none 79 | # Modifies: none 80 | # Effects: none 81 | # Returns: self.combos 82 | # Throws: none 83 | def strat_combs(self): 84 | #print(self.combos) 85 | return self.combos 86 | 87 | 88 | # Overview: Function that returns the user's total all time return 89 | # 90 | # Params: self (PortfolioManager Object) 91 | # Requires: None 92 | # Modifies: None 93 | # Effects: None 94 | # Returns: Returns the total all time return of the user's account 95 | def get_total_return(self): 96 | pass 97 | 98 | # Overview: Function that returns the user's return for the current day 99 | # 100 | # Params: self (PortfolioManager Object) 101 | # Requires: None 102 | # Modifies: None 103 | # Effects: None 104 | # Returns: Returns the return of the current day for this user's account 105 | def get_todays_return(self): 106 | pass 107 | 108 | # Overview: Function that returns current running strategy/strategies 109 | # 110 | # Params: self (PortfolioManager Object) 111 | # Requires: None 112 | # Modifies: None 113 | # Effects: None 114 | # Returns: Returns a list of length 0...n depending on the number of 115 | # strategies the user is currently using. This list contains 116 | # the current strategies. 117 | def get_strat(self): 118 | print(self.strategies) 119 | return self.strategies 120 | 121 | # Overview: Prints current strategy 122 | # 123 | # Requires: none 124 | # Modifies: none 125 | # Effects: none 126 | # Returns: self.input 127 | # Throws: none 128 | # TODO: 129 | def get_current_strat(self): 130 | print(self.input) 131 | return self.input 132 | 133 | # Overview: Function that computes a portfolio diversity score based 134 | # on the current holdings the user has. 135 | # 136 | # Params: self (PortfolioManager Object) 137 | # Requires: The user has at least one holding 138 | # Modifies: None 139 | # Effects: None 140 | # Returns: Returns a number between 0 and 100, 0 meaning the user only 141 | # has one stock and 100 meaning the user's portfolio is very 142 | # diverse. The score is computed based on strategies being 143 | # used the number and diversity of tickers being traded. 144 | def get_diversity_score(self): 145 | pass 146 | 147 | # Overview: Function that returns the user's remaining total of 148 | # liquid cash left in their account. 149 | # 150 | # Params: self (PortfolioManager Object) 151 | # Requires: None 152 | # Modifies: None 153 | # Effects: None 154 | # Returns: A float representing the liquid cash in the user's account. 155 | # TODO: Add functionality for other apis 156 | def get_current_liquid_cash(self): 157 | print("Liquid Balance: {:.2f}".format(self.liquid)) 158 | return self.liquid 159 | 160 | # Overview: Function that returns the user's current total value they have 161 | # vested in stocks. 162 | # 163 | # Params: self (PortfolioManager Object) 164 | # Requires: None 165 | # Modifies: None 166 | # Effects: None 167 | # Returns: A float representing the value of all of the user's stocks. 168 | def get_current_total_stock_value(self): 169 | pass 170 | 171 | # Overview: Function that returns the user's current total value they have 172 | # vested in crypto. 173 | # 174 | # Params: self (PortfolioManager Object) 175 | # Requires: None 176 | # Modifies: None 177 | # Effects: None 178 | # Returns: A float representing the value of all of the user's crypto holdings. 179 | def get_current_total_crypto_value(self): 180 | pass 181 | 182 | # Overview: Function that displays the entire balance of a user 183 | # 184 | # Params: self (PortfolioManager Object) 185 | # Requires: None 186 | # Modifies: None 187 | # Effects: None 188 | # Returns: Returns a number reflecting entire balance of a user 189 | # TODO: Add functionality for other API's 190 | def get_balance(self): 191 | pass 192 | 193 | # Overview: Displays entire order history of a user's account 194 | # 195 | # Params: self (PortfolioManager Object) 196 | # Requires: None 197 | # Modifies: None 198 | # Effects: None 199 | # Returns: Creates chart displaying all orders from a user account 200 | def order_history(self): 201 | orders = self.api.list_orders(status='closed') 202 | columns = ["Symbol","Type","Date","Shares","Price per Share", 203 | "Notional","Amount","Status"] 204 | table = {i: [] for i in columns} 205 | for i in orders: 206 | table['Symbol'].append(i.symbol) 207 | table['Type'].append(i.side) 208 | table['Date'].append(str(i.created_at)[:10]) 209 | table['Shares'].append(i.qty) 210 | table['Price per Share'].append(i.filled_avg_price) 211 | table['Notional'].append(i.notional) 212 | if(i.status == 'filled'): 213 | table['Amount'].append('{:.2f}'.format(float(i.filled_qty)* 214 | float(i.filled_avg_price))) 215 | else: 216 | table['Amount'].append(0) 217 | table['Status'].append(i.status) 218 | df = pd.DataFrame(table) 219 | print(df) 220 | 221 | # Overview: Displays all positions of a user's account 222 | # 223 | # Params: self (PortfolioManager Object) 224 | # Requires: None 225 | # Modifies: None 226 | # Effects: None 227 | # Returns: Creates chart displaying all positions from a user account 228 | def check_positions(self): 229 | pos = self.api.list_positions() 230 | col = ["Symbol","Avg Buy","Profit","Qty","Daily Change","Current Price", 231 | "Last Day Price","Market Value"] 232 | table = {i: [] for i in col} 233 | for i in pos: 234 | table['Symbol'].append(i.symbol) 235 | table['Avg Buy'].append("{:.2f}".format(float(i.avg_entry_price))) 236 | table['Qty'].append(i.qty) 237 | table['Daily Change'].append("{:.5f}".format( 238 | float(i.change_today))) 239 | table['Current Price'].append(i.current_price) 240 | table['Last Day Price'].append(i.lastday_price) 241 | table['Market Value'].append(i.market_value) 242 | table['Profit'].append("{:.2f}".format(float(i.qty)* 243 | (float(i.current_price)-float(i.avg_entry_price)))) 244 | df = pd.DataFrame(table) 245 | print(df) 246 | # ====================Producers==================== 247 | # ====================Mutators==================== 248 | # Overview: Adds current strategy to self.strategies 249 | # 250 | # Requires: none 251 | # Modifies: self.strategies 252 | # Effects: self.strategies.append(self.input) 253 | # Returns: self.strategies 254 | # Throws: none 255 | # TODO: 256 | def add_strat(self): 257 | self.strategies.append(self.input.copy()) 258 | return self.strategies 259 | 260 | # Overview: Sets the initial values of input 261 | # 262 | # Requires: none 263 | # Modifies: self.input 264 | # Effects: val of self.input changes based on user input 265 | # Returns: none 266 | # Throws: none 267 | # TODO: Add functionality for other api's 268 | def initial_setup(self): 269 | print("Enter the specified Strategy: (1-9)") 270 | strat = int(input()) 271 | name = Strategies(strat).name 272 | print("Selected Strategy: {}".format(name)) 273 | print("Enter the specified Risk: (1-9)") 274 | risk = int(input()) 275 | name = Risk(risk).name 276 | print("Selected Risk: {}".format(name)) 277 | print("Enter the specified DH_API: (1-5)") 278 | dh_api = int(input()) 279 | name = DH_API(dh_api).name 280 | print("Selected DH_API: {}".format(name)) 281 | print("Enter the specified EH_API: (1-4)") 282 | eh_api = int(input()) 283 | name = EH_API(eh_api).name 284 | print("Selected EH_API: {}".format(name)) 285 | print("Enter the specified Stop Loss: (1-2)") 286 | stop_loss = int(input()) 287 | name = Stop_loss(stop_loss).name 288 | print("Selected Stop Loss: {}".format(name)) 289 | self.input = [strat, risk, dh_api, eh_api, stop_loss] 290 | self.add_strat() 291 | if(self.input[3] == 1): 292 | self.set_alpaca() 293 | 294 | # Overview: Sets Alpaca API values if alpaca api is selected for EH 295 | # 296 | # Requires: none 297 | # Modifies: self.headers, self.account_url, self.order_url 298 | # Effects: self.headers takes the api key/secret key and formats to alpaca format 299 | # self.account_url takes base url and formats to alpaca format 300 | # self.order_url takes base url and formats to alpaca format 301 | # Returns: none 302 | # Throws: none 303 | def set_alpaca(self): 304 | self.headers = { 305 | "APCA-API-KEY-ID": self.api_key, 306 | "APCA-API-SECRET-KEY": self.secret_key 307 | } 308 | self.api = tradeapi.REST(self.headers["APCA-API-KEY-ID"], 309 | self.headers["APCA-API-SECRET-KEY"], self.base_url) 310 | self.account_url = "{}/v2/account".format(self.base_url) 311 | self.order_url = "{}/v2/orders".format(self.base_url) 312 | self.liquid = float(self.api.get_account().cash) 313 | self.balance = float(self.api.get_account().portfolio_value) 314 | self.assets = self.balance - self.liquid 315 | 316 | # Overview: Changes the strategy based on user input 317 | # 318 | # Requires: none 319 | # Modifies: self.input[0] 320 | # Effects: self.input[0] changes based on user input 321 | # Returns: none 322 | # Throws: none 323 | # TODO: Add functionality to stop one strategy and start new one 324 | def change_strat(self): 325 | print("Enter new Strategy to use: (1-9)") 326 | strat = int(input()) 327 | name = Strategies(strat).name 328 | print("Selected Strategy: {}".format(name)) 329 | self.input[0] = strat 330 | self.add_strat() 331 | 332 | # Overview: Changes the risk based on user input 333 | # 334 | # Requires: none 335 | # Modifies: self.input[1] 336 | # Effects: self.input[1] changes based on user input 337 | # Returns: none 338 | # Throws: none 339 | # TODO: Add functionality to change risk value used 340 | def change_risk(self): 341 | print("Enter new Risk to use: (1-9)") 342 | risk = int(input()) 343 | name = Risk(risk).name 344 | print("Selected Risk: {}".format(name)) 345 | self.input[1] = risk 346 | self.add_strat() 347 | 348 | # Overview: Adds a stock ticker or strategy to the user's watch list. 349 | # 350 | # Requires: none 351 | # Modifies: self.watch_list 352 | # Effects: adds a ticker to self.watch_list 353 | # Returns: none 354 | # Throws: none 355 | def add_to_watch_list(self, strat, ticker): 356 | pass 357 | 358 | # Overview: Removes a stock ticker or strategy from the user's watch list. 359 | # 360 | # Requires: none 361 | # Modifies: self.watch_list 362 | # Effects: removes a ticker from self.watch_list 363 | # Returns: none 364 | # Throws: none 365 | def remove_from_watch_list(self, strat, ticker): 366 | pass 367 | 368 | 369 | # Overview: Function that withdraws money from balance 370 | # 371 | # Params: None 372 | # Requires: None 373 | # Modifies: this.balance 374 | # Effects: this.balance -= minus 375 | # Returns: Returns a number reflecting new balance 376 | # Throws: Assertion if withdraw is greater than balance 377 | # TODO: Add functionality to actually withdraw to various API Accounts 378 | def withdraw(self): 379 | print("Current Liquid Balance: {:0.2f}".format(self.liquid)) 380 | print("Enter a value to withdraw") 381 | user = int(input()) 382 | while(user > self.liquid): 383 | print("Withdraw exceeds balance") 384 | user = int(input()) 385 | self.liquid -= user 386 | print("New Liquid Balance: {:0.2f}".format(self.liquid)) 387 | return self.liquid 388 | 389 | # Overview: Function that adds money to balance 390 | # 391 | # Params: self (PortfolioManager Object) 392 | # Requires: None 393 | # Modifies: this.balance 394 | # Effects: this.balance += plus 395 | # Returns: Returns a number reflecting entire balance of a user 396 | # TODO: Add functionality to actually deposit to various API Accounts 397 | def deposit(self): 398 | print("Current Balance: {:0.2f}".format(self.balance)) 399 | print("Enter a value to deposit") 400 | user = int(input()) 401 | self.liquid += user 402 | self.balance += user 403 | print("New Balance: {:0.2f}".format(self.balance)) 404 | return self.liquid 405 | 406 | -------------------------------------------------------------------------------- /src/ExecutionHandler.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import json 3 | import alpaca_trade_api as tradeapi 4 | from abc import ABC, abstractmethod 5 | 6 | 7 | class ExecutionHandler(ABC): 8 | 9 | """ 10 | Overview: Abstract Class for the ExecutionHandler. Outlines essential 11 | function signatures. 12 | """ 13 | # ====================Creators==================== 14 | 15 | @abstractmethod 16 | def __init__(self): 17 | pass 18 | # ====================Observers==================== 19 | 20 | @abstractmethod 21 | def get_account(self): 22 | pass 23 | 24 | @abstractmethod 25 | def get_orders(self): 26 | pass 27 | 28 | @abstractmethod 29 | def check_limitations(self): 30 | pass 31 | 32 | def cancel_order(self): 33 | pass 34 | 35 | def cancel_all_orders(self): 36 | pass 37 | 38 | def get_open_pos(self): 39 | pass 40 | 41 | @abstractmethod 42 | def close_pos(self): 43 | pass 44 | 45 | @abstractmethod 46 | def close_all_pos(self): 47 | pass 48 | 49 | @abstractmethod 50 | def bracket_order(self): 51 | pass 52 | 53 | @abstractmethod 54 | def dynamic_stop_loss(self): 55 | pass 56 | 57 | @abstractmethod 58 | def fill_order(self): 59 | pass 60 | 61 | # ====================Mutators==================== 62 | 63 | @abstractmethod 64 | def create_order(self): 65 | pass 66 | 67 | def create_order_notional(self): 68 | pass 69 | 70 | @abstractmethod 71 | def trade_signal(self): 72 | pass 73 | 74 | @abstractmethod 75 | def money_alloc_pre(self): 76 | pass 77 | 78 | @abstractmethod 79 | def money_alloc_post(self): 80 | pass 81 | 82 | @abstractmethod 83 | def replace_order(self): 84 | pass 85 | 86 | 87 | class AlpacaExecutionHandler(ExecutionHandler): 88 | 89 | """ 90 | Overview: ExecutionHandler class: ExecutionHandler is a class that takes in 91 | data from given brokerage API, receives signals from the 92 | StrategyHandler, and puts in buy/sell order requests through 93 | said API. StrategyHandler determines which stock to buy. 94 | ExecutionHandler decides how many shares to buy based on 95 | information passed in from the StrategyHandler. The symbol, 96 | quantity or notional, and current OrderID are stored as 97 | local variables. 98 | """ 99 | 100 | # ====================Creators==================== 101 | 102 | def __init__(self, API_key_id, API_secret_key, base_url): 103 | self.base_url = base_url 104 | self.account_url = "{}/v2/account".format(self.base_url) 105 | self.order_url = "{}/v2/orders".format(self.base_url) 106 | self.headers = {"APCA-API-KEY-ID": API_key_id, 107 | "APCA-API-SECRET-KEY": API_secret_key} 108 | self.api = tradeapi.REST(self.headers["APCA-API-KEY-ID"], 109 | self.headers["APCA-API-SECRET-KEY"], base_url) 110 | self.api_account = self.api.get_account() 111 | self.account_cash = self.api_account.cash 112 | self.position_url = "{}/v2/positions".format(self.base_url) 113 | self.symbol = None 114 | self.qty = 0 115 | self.orderID = None 116 | # ====================Observers==================== 117 | 118 | # overview: returns the alpaca api account 119 | # 120 | # modifies: nothing. 121 | # effects: nothing. 122 | # returns: alpaca api account object 123 | def get_account(self): 124 | return self.api_account 125 | 126 | # overview: checks if alpaca account has ability to trade or not 127 | # 128 | # modifies: none 129 | # effects: none 130 | # returns: True if account is able to trade. False otherwise 131 | def check_limitations(self): 132 | if(self.api_account.trading_blocked or 133 | self.api_account.account_blocked or float(self.account_cash) <= 0): 134 | return False 135 | return True 136 | 137 | # overview: cancels an order based on specified orderid 138 | # 139 | # params: orderid = alpaca order id for a particular trade 140 | # modifies: none 141 | # effects: none 142 | # returns: json containing order details 143 | def cancel_order(self, orderid): 144 | delete_url = self.order_url + '/' + orderid 145 | r = requests.delete(delete_url, headers=self.headers) 146 | return json.loads(r.content) 147 | 148 | # overview: cancels all existing orders 149 | # 150 | # modifies: none 151 | # effects: none 152 | # returns: json containing order details 153 | def cancel_all_orders(self): 154 | r = requests.delete(order_url, headers=self.headers) 155 | return json.loads(r.content) 156 | 157 | # overview: get details of an open position based on specified symbol 158 | # 159 | # params: symbol = symbol to check positions 160 | # modifies: none 161 | # effects: none 162 | # returns: json containing order details 163 | def get_open_pos(self, symbol): 164 | open_pos_url = self.position_url + '/' + symbol 165 | r = requests.get(open_pos_url, headers=self.headers) 166 | return json.loads(r.content) 167 | 168 | # overview: closes a position based on specified symbol 169 | # 170 | # params: symbol = symbol to close 171 | # modifies: none 172 | # effects: none 173 | # returns: json containing order details 174 | def close_pos(self, symbol): 175 | close_pos_url = self.position_url + '/' + symbol 176 | r = requests.delete(close_pos_url, headers=self.headers) 177 | return json.loads(r.content) 178 | 179 | # overview: closes all positions 180 | # 181 | # params: none 182 | # modifies: none 183 | # effects: none 184 | # returns: json containing order details 185 | def close_all_pos(self): 186 | r = requests.delete(self.position_url, headers=self.headers) 187 | return json.loads(r.content) 188 | 189 | # overview: creates a quantity bracket order based on specified parameters 190 | # 191 | # params: symbol = stock symbol to identify asset to trade 192 | # qty = number of shares to trade 193 | # side = buy or sell 194 | # loss = specified stop loss price 195 | # profit = specified profit price 196 | # limit = specified limit price. None default 197 | # modifies: none 198 | # effects: none 199 | # returns: json containing order details 200 | def bracket_order(self, symbol, qty, side, loss, limit=None): 201 | stop_loss = { 202 | "stop_price": loss, 203 | "limit_price": limit 204 | } 205 | r = self.create_order(symbol, qty, side, "market", "gtc", 206 | "oto", None, stop_loss) 207 | return r 208 | 209 | # overview: dynamically adjusts the stop loss value of an order based on 210 | # incoming data. stop value should never decrease 211 | # 212 | # params: stop = stop loss number 213 | # limit = stop limit number. Default none 214 | # modifies: none 215 | # effects: none 216 | # returns: json containing order details 217 | def dynamic_stop_loss(self, stop, limit=None): 218 | self.get_orders() 219 | r = self.replace_order(self.orderID, self.qty, "gtc", limit, stop) 220 | return r 221 | 222 | # overview: loops until order is filled 223 | # 224 | # params: orderID is the order ID which identifies which order to wait 225 | # for fill 226 | # modifies: none 227 | # effects: none 228 | # returns: none 229 | def fill_order(self, orderID): 230 | order = self.order_url + '/' + orderID 231 | r = requests.get(order, headers=self.headers) 232 | status = json.loads(r.content)['status'] 233 | while(status != "filled"): 234 | r = requests.get(order, headers=self.headers) 235 | status = json.loads(r.content)['status'] 236 | return 237 | 238 | # ====================Producers==================== 239 | 240 | # ====================Mutators==================== 241 | 242 | # overview: Places a quantity order based on parameters and updates 243 | # variables accordingly. 244 | # 245 | # params: symbol = stock symbol to identify asset to trade 246 | # qty = number of shares to trade 247 | # side = buy or sell 248 | # type = market, limit,stop, stop_limit, trailing_stop 249 | # time_in_force = day, gtc, opg, cls, ioc, fok 250 | # limit_price = limit price. required if type is limit, stop_limit 251 | # stop_price = stop price. required of type is stop, stop_limit 252 | # trail_price = trail price. required if type is trailing_stop 253 | # trail_percent = trail percent. required if type is trailing_stop 254 | # extended_hours = Boolean. If true, order will be eligible to 255 | # execute in premarket/afterhours. Only works with 256 | # type limit and time_in_force day 257 | # client_order_id = unique identifier for order. Automatically 258 | # generated if not sent 259 | # take_profit = dictionary containing limit_price 260 | # stop_loss = dictionary containing stop_price and limit_price 261 | # modifies: self.orderID, self.symbol, self.qty 262 | # effects: self.orderID = alpaca generated order id 263 | # self.symbol = symbol 264 | # self.qty = qty 265 | # returns: json containing order details 266 | def create_order(self, symbol, qty, side, type, time_in_force, 267 | order_class=None, take_profit=None, stop_loss=None, 268 | limit_price=None, stop_price=None, 269 | trail_price=None, trail_percent=None, 270 | extended_hours=False, client_order_id=None): 271 | data = { 272 | "symbol": symbol, 273 | "qty": qty, 274 | "side": side, 275 | "type": type, 276 | "time_in_force": time_in_force, 277 | "limit_price": limit_price, 278 | "stop_price": stop_price, 279 | "trail_price": trail_price, 280 | "trail_percent": trail_percent, 281 | "extended_hours": extended_hours, 282 | "client_order_id": client_order_id, 283 | "order_class": order_class, 284 | "take_profit": take_profit, 285 | "stop_loss": stop_loss 286 | } 287 | 288 | r = requests.post(self.order_url, json=data, headers=self.headers) 289 | self.orderID = json.loads(r.content)['id'] 290 | self.symbol = symbol 291 | self.qty = qty 292 | self.fill_order(self.orderID) 293 | return json.loads(r.content) 294 | 295 | # overview: Places a notional order based on parameters and updates 296 | # variables accordingly. 297 | # 298 | # params: symbol = stock symbol to identify asset to trade 299 | # notional = dollar amount to trade. 300 | # side = buy or sell 301 | # limit_price = limit price. required if type is limit, stop_limit 302 | # stop_price = stop price. required of type is stop or stop_limit 303 | # trail_price = trail price. required if type is trailing_stop 304 | # trail_percent = trail percent. required if type is trailing_stop 305 | # extended_hours = Boolean. If true, order will be eligible to 306 | # execute in premarket/afterhours. Only works with 307 | # type limit and time_in_force day 308 | # client_order_id = unique identifier for order. Automatically 309 | # generated if not sent 310 | # take_profit = dictionary containing limit_price 311 | # stop_loss = dictionary containing stop_price and limit_price 312 | # modifies: self.orderID, self.symbol, self.qty 313 | # effects: self.orderID = alpaca generated order id 314 | # self.symbol = symbol 315 | # self.qty = qty 316 | # returns: json containing order details 317 | def create_order_notional(self, symbol, notional, side, order_class=None, 318 | take_profit=None, stop_loss=None, 319 | limit_price=None, stop_price=None, 320 | trail_price=None, trail_percent=None, 321 | extended_hours=False, client_order_id=None): 322 | data = { 323 | "symbol": symbol, 324 | "notional": notional, 325 | "side": side, 326 | "type": 'market', 327 | "time_in_force": 'day', 328 | "limit_price": limit_price, 329 | "stop_price": stop_price, 330 | "trail_price": trail_price, 331 | "trail_percent": trail_percent, 332 | "extended_hours": extended_hours, 333 | "client_order_id": client_order_id, 334 | "order_class": order_class, 335 | "take_profit": take_profit, 336 | "stop_loss": stop_loss 337 | } 338 | 339 | r = requests.post(self.order_url, json=data, headers=self.headers) 340 | self.orderID = json.loads(r.content)['id'] 341 | self.symbol = symbol 342 | self.fill_order(self.orderID) 343 | return json.loads(r.content) 344 | 345 | # overview: gets all active orders 346 | # 347 | # modifies: self.orderID 348 | # effects: self.orderID = most recent orderID if available 349 | # returns: json containing all orders 350 | def get_orders(self): 351 | r = requests.get(self.order_url, headers=self.headers) 352 | length = len(json.loads(r.content)) 353 | self.orderID = json.loads(r.content)[length-1]['id'] 354 | self.qty = json.loads(r.content)[length-1]['qty'] 355 | return json.loads(r.content) 356 | 357 | # overview: 358 | # 359 | # modifies: 360 | # effects: 361 | # returns: 362 | def trade_signal(self): 363 | """ 364 | Takes data from stategy handler and buys/sells stocks based on strategy 365 | """ 366 | return 367 | 368 | # overview: determines number of shares to buy based on explicitly 369 | # specified cap value and risk value 370 | # 371 | # params: cap = percent of account balance to risk 372 | # risk = stop loss amount. Ex. If share is 100 and stop_loss = 95, 373 | # risk is 100-95=5 374 | # modifies: self.qty 375 | # effects: self.qty = number of shares calculated based on capped formula 376 | # returns: self.qty 377 | def money_alloc_pre(self, cap, risk): 378 | shares = (self.account.cash*cap)/risk 379 | shares = int(shares//1) 380 | self.qty = shares 381 | return self.qty 382 | 383 | # overview: determines number of shares to buy based on win rate and 384 | # win/loss ratio determined from historical data 385 | # 386 | # params: win = win rate. 387 | # ratio = win/loss rate 388 | # modifies: self.qty 389 | # effects: self.qty = number of shares calculated based on Kelly Criterion 390 | # returns: self.qty 391 | def money_alloc_post(self, win, ratio): 392 | k = win - ((1-win)/ratio) 393 | shares = self.account.cash*k 394 | shares = int(shares//1) 395 | self.qty = shares 396 | return self.qty 397 | 398 | # overview: replaces an order based on specified parameters 399 | # 400 | # params: orderid = alpaca order id for a particular trade 401 | # qty = new updated quantity if changed 402 | # time_in_force = day, gtc, opg, cls, ioc, fok 403 | # limit_price = limit price. required if type is limit, stop_limit 404 | # stop_price = stop price. required of type is stop or stop_limit 405 | # trail = new value of trail_price or trail_percent value 406 | # client_order_id = unique identifier for order. Automatically 407 | # generated if not sent 408 | # modifies: self.qty 409 | # effects: self.qty = updated quantity if changed 410 | # returns: json containing new order details 411 | def replace_order(self, orderid, qty, time_in_force, limit_price=None, 412 | stop_price=None, trail=None, client_order_id=None): 413 | replace_url = self.order_url + '/' + orderid 414 | data = { 415 | "qty": qty, 416 | "time_in_force": time_in_force, 417 | "limit_price": limit_price, 418 | "stop_price": stop_price, 419 | "trail": trail, 420 | "client_order_id": client_order_id, 421 | } 422 | r = requests.patch(replace_url, json=data, headers=self.headers) 423 | return json.loads(r.content) 424 | -------------------------------------------------------------------------------- /test/data/AAPL.csv: -------------------------------------------------------------------------------- 1 | Date,Open,High,Low,Close,Adj Close,Volume 2 | 2020-04-20,69.487503,70.419998,69.212502,69.232498,68.699974,130015200 3 | 2020-04-21,69.070000,69.312500,66.357498,67.092499,66.576424,180991600 4 | 2020-04-22,68.402496,69.474998,68.050003,69.025002,68.494064,117057200 5 | 2020-04-23,68.967499,70.437500,68.717499,68.757500,68.228622,124814400 6 | 2020-04-24,69.300003,70.752502,69.250000,70.742500,70.198349,126508800 7 | 2020-04-27,70.449997,71.135002,69.987503,70.792503,70.247971,117087600 8 | 2020-04-28,71.269997,71.457497,69.550003,69.644997,69.109299,112004800 9 | 2020-04-29,71.182503,72.417503,70.972504,71.932503,71.379196,137280800 10 | 2020-04-30,72.489998,73.632500,72.087502,73.449997,72.885025,183064000 11 | 2020-05-01,71.562500,74.750000,71.462502,72.267502,71.711632,240616800 12 | 2020-05-04,72.292503,73.422501,71.580002,73.290001,72.726265,133568000 13 | 2020-05-05,73.764999,75.250000,73.614998,74.389999,73.817802,147751200 14 | 2020-05-06,75.114998,75.809998,74.717499,75.157501,74.579391,142333600 15 | 2020-05-07,75.805000,76.292503,75.492500,75.934998,75.350914,115215200 16 | 2020-05-08,76.410004,77.587502,76.072502,77.532501,77.144394,134048000 17 | 2020-05-11,77.025002,79.262497,76.809998,78.752502,78.358284,145946400 18 | 2020-05-12,79.457497,79.922501,77.727501,77.852501,77.462791,162301200 19 | 2020-05-13,78.037498,78.987503,75.802498,76.912498,76.527496,200622400 20 | 2020-05-14,76.127502,77.447502,75.382500,77.385002,76.997635,158929200 21 | 2020-05-15,75.087502,76.974998,75.052498,76.927498,76.542412,166348400 22 | 2020-05-18,78.292503,79.125000,77.580002,78.739998,78.345848,135372400 23 | 2020-05-19,78.757500,79.629997,78.252502,78.285004,77.893127,101729600 24 | 2020-05-20,79.169998,79.879997,79.129997,79.807503,79.408005,111504800 25 | 2020-05-21,79.665001,80.222504,78.967499,79.212502,78.815979,102688800 26 | 2020-05-22,78.942497,79.807503,78.837502,79.722504,79.323433,81803200 27 | 2020-05-26,80.875000,81.059998,79.125000,79.182503,78.786133,125522000 28 | 2020-05-27,79.035004,79.677498,78.272499,79.527496,79.129402,112945200 29 | 2020-05-28,79.192497,80.860001,78.907501,79.562500,79.164230,133560800 30 | 2020-05-29,79.812500,80.287498,79.117500,79.485001,79.087120,153598000 31 | 2020-06-01,79.437500,80.587502,79.302498,80.462502,80.059723,80791200 32 | 2020-06-02,80.187500,80.860001,79.732498,80.834999,80.430351,87642800 33 | 2020-06-03,81.165001,81.550003,80.574997,81.279999,80.873131,104491200 34 | 2020-06-04,81.097504,81.404999,80.195000,80.580002,80.176636,87560400 35 | 2020-06-05,80.837502,82.937500,80.807503,82.875000,82.460152,137250400 36 | 2020-06-08,82.562500,83.400002,81.830002,83.364998,82.947685,95654400 37 | 2020-06-09,83.035004,86.402496,83.002502,85.997498,85.567009,147712400 38 | 2020-06-10,86.974998,88.692497,86.522499,88.209999,87.768440,166651600 39 | 2020-06-11,87.327499,87.764999,83.870003,83.974998,83.554642,201662400 40 | 2020-06-12,86.180000,86.949997,83.555000,84.699997,84.276009,200146000 41 | 2020-06-15,83.312500,86.419998,83.144997,85.747498,85.318260,138808800 42 | 2020-06-16,87.864998,88.300003,86.180000,88.019997,87.579391,165428800 43 | 2020-06-17,88.787498,88.849998,87.772499,87.897499,87.457504,114406400 44 | 2020-06-18,87.852501,88.362503,87.305000,87.932503,87.492332,96820400 45 | 2020-06-19,88.660004,89.139999,86.287498,87.430000,86.992348,264476000 46 | 2020-06-22,87.834999,89.864998,87.787498,89.717499,89.268394,135445200 47 | 2020-06-23,91.000000,93.095001,90.567497,91.632500,91.173805,212155600 48 | 2020-06-24,91.250000,92.197502,89.629997,90.014999,89.564407,192623200 49 | 2020-06-25,90.175003,91.250000,89.392502,91.209999,90.753426,137522400 50 | 2020-06-26,91.102501,91.330002,88.254997,88.407501,87.964951,205256800 51 | 2020-06-29,88.312500,90.542503,87.820000,90.445000,89.992256,130646000 52 | 2020-06-30,90.019997,91.495003,90.000000,91.199997,90.743469,140223200 53 | 2020-07-01,91.279999,91.839996,90.977501,91.027496,90.571838,110737200 54 | 2020-07-02,91.962502,92.617500,90.910004,91.027496,90.571838,114041600 55 | 2020-07-06,92.500000,93.945000,92.467499,93.462502,92.994652,118655600 56 | 2020-07-07,93.852501,94.654999,93.057503,93.172501,92.706093,112424400 57 | 2020-07-08,94.180000,95.375000,94.089996,95.342499,94.865234,117092000 58 | 2020-07-09,96.262497,96.317497,94.672501,95.752502,95.273193,125642800 59 | 2020-07-10,95.334999,95.980003,94.705002,95.919998,95.439842,90257200 60 | 2020-07-13,97.264999,99.955002,95.257500,95.477501,94.999557,191649200 61 | 2020-07-14,94.839996,97.254997,93.877502,97.057503,96.571655,170989200 62 | 2020-07-15,98.989998,99.247498,96.489998,97.724998,97.235809,153198000 63 | 2020-07-16,96.562500,97.404999,95.904999,96.522499,96.039330,110577600 64 | 2020-07-17,96.987503,97.147499,95.839996,96.327499,95.845299,92186800 65 | 2020-07-20,96.417503,98.500000,96.062500,98.357498,97.865143,90318000 66 | 2020-07-21,99.172501,99.250000,96.742500,97.000000,96.514442,103646000 67 | 2020-07-22,96.692497,97.974998,96.602501,97.272499,96.785568,89001600 68 | 2020-07-23,96.997498,97.077499,92.010002,92.845001,92.380234,197004400 69 | 2020-07-24,90.987503,92.970001,89.144997,92.614998,92.151382,185438800 70 | 2020-07-27,93.709999,94.904999,93.480003,94.809998,94.335396,121214000 71 | 2020-07-28,94.367500,94.550003,93.247498,93.252502,92.785706,103625600 72 | 2020-07-29,93.750000,95.230003,93.712502,95.040001,94.564247,90329200 73 | 2020-07-30,94.187500,96.297501,93.767502,96.190002,95.708496,158130000 74 | 2020-07-31,102.885002,106.415001,100.824997,106.260002,105.728088,374336800 75 | 2020-08-03,108.199997,111.637497,107.892502,108.937500,108.392181,308151200 76 | 2020-08-04,109.132500,110.790001,108.387497,109.665001,109.116043,173071600 77 | 2020-08-05,109.377502,110.392502,108.897499,110.062500,109.511551,121992000 78 | 2020-08-06,110.404999,114.412498,109.797501,113.902496,113.332329,202428800 79 | 2020-08-07,113.205002,113.675003,110.292503,111.112503,110.755638,198045600 80 | 2020-08-10,112.599998,113.775002,110.000000,112.727501,112.365448,212403600 81 | 2020-08-11,111.970001,112.482498,109.107498,109.375000,109.023705,187902400 82 | 2020-08-12,110.497498,113.275002,110.297501,113.010002,112.647041,165944800 83 | 2020-08-13,114.430000,116.042503,113.927498,115.010002,114.640610,210082000 84 | 2020-08-14,114.830002,115.000000,113.044998,114.907501,114.538445,165565200 85 | 2020-08-17,116.062500,116.087502,113.962502,114.607498,114.239403,119561600 86 | 2020-08-18,114.352501,116.000000,114.007500,115.562500,115.191338,105633600 87 | 2020-08-19,115.982498,117.162498,115.610001,115.707497,115.335869,145538000 88 | 2020-08-20,115.750000,118.392502,115.732498,118.275002,117.895126,126907200 89 | 2020-08-21,119.262497,124.867500,119.250000,124.370003,123.970551,338054800 90 | 2020-08-24,128.697495,128.785004,123.937500,125.857498,125.453270,345937600 91 | 2020-08-25,124.697502,125.180000,123.052498,124.824997,124.424088,211495600 92 | 2020-08-26,126.180000,126.992500,125.082497,126.522499,126.116135,163022400 93 | 2020-08-27,127.142502,127.485001,123.832497,125.010002,124.608498,155552400 94 | 2020-08-28,126.012497,126.442497,124.577499,124.807503,124.406647,187630000 95 | 2020-08-31,127.580002,131.000000,126.000000,129.039993,128.625549,225702700 96 | 2020-09-01,132.759995,134.800003,130.529999,134.179993,133.749039,152470100 97 | 2020-09-02,137.589996,137.979996,127.000000,131.399994,130.977966,200119000 98 | 2020-09-03,126.910004,128.839996,120.500000,120.879997,120.491753,257599600 99 | 2020-09-04,120.070000,123.699997,110.889999,120.959999,120.571503,332607200 100 | 2020-09-08,113.949997,118.989998,112.680000,112.820000,112.457649,231366600 101 | 2020-09-09,117.260002,119.139999,115.260002,117.320000,116.943192,176940500 102 | 2020-09-10,120.360001,120.500000,112.500000,113.489998,113.125496,182274400 103 | 2020-09-11,114.570000,115.230003,110.000000,112.000000,111.640282,180860300 104 | 2020-09-14,114.720001,115.930000,112.800003,115.360001,114.989487,140150100 105 | 2020-09-15,118.330002,118.830002,113.610001,115.540001,115.168915,184642000 106 | 2020-09-16,115.230003,116.000000,112.040001,112.129997,111.769859,154679000 107 | 2020-09-17,109.720001,112.199997,108.709999,110.339996,109.985611,178011000 108 | 2020-09-18,110.400002,110.879997,106.089996,106.839996,106.496849,287104900 109 | 2020-09-21,104.540001,110.190002,103.099998,110.080002,109.726448,195713800 110 | 2020-09-22,112.680000,112.860001,109.160004,111.809998,111.450882,183055400 111 | 2020-09-23,111.620003,112.110001,106.769997,107.120003,106.775963,150718700 112 | 2020-09-24,105.169998,110.250000,105.000000,108.220001,107.872421,167743300 113 | 2020-09-25,108.430000,112.440002,107.669998,112.279999,111.919373,149981400 114 | 2020-09-28,115.010002,115.320000,112.779999,114.959999,114.590767,137672400 115 | 2020-09-29,114.550003,115.309998,113.570000,114.089996,113.723564,99382200 116 | 2020-09-30,113.790001,117.260002,113.620003,115.809998,115.438042,142675200 117 | 2020-10-01,117.639999,117.720001,115.830002,116.790001,116.414894,116120400 118 | 2020-10-02,112.889999,115.370003,112.220001,113.019997,112.657005,144712000 119 | 2020-10-05,113.910004,116.650002,113.550003,116.500000,116.125824,106243800 120 | 2020-10-06,115.699997,116.120003,112.250000,113.160004,112.796555,161498200 121 | 2020-10-07,114.620003,115.550003,114.129997,115.080002,114.710388,96849000 122 | 2020-10-08,116.250000,116.400002,114.589996,114.970001,114.600739,83477200 123 | 2020-10-09,115.279999,117.000000,114.919998,116.970001,116.594315,100506900 124 | 2020-10-12,120.059998,125.180000,119.279999,124.400002,124.000458,240226800 125 | 2020-10-13,125.269997,125.389999,119.650002,121.099998,120.711044,262330500 126 | 2020-10-14,121.000000,123.029999,119.620003,121.190002,120.800766,151062300 127 | 2020-10-15,118.720001,121.199997,118.150002,120.709999,120.322304,112559200 128 | 2020-10-16,121.279999,121.550003,118.809998,119.019997,118.637726,115393800 129 | 2020-10-19,119.959999,120.419998,115.660004,115.980003,115.607498,120639300 130 | 2020-10-20,116.199997,118.980003,115.629997,117.510002,117.132591,124423700 131 | 2020-10-21,116.669998,118.709999,116.449997,116.870003,116.494644,89946000 132 | 2020-10-22,117.449997,118.040001,114.589996,115.750000,115.378235,101988000 133 | 2020-10-23,116.389999,116.550003,114.279999,115.040001,114.670517,82572600 134 | 2020-10-26,114.010002,116.550003,112.879997,115.050003,114.680489,111850700 135 | 2020-10-27,115.489998,117.279999,114.540001,116.599998,116.225510,92276800 136 | 2020-10-28,115.050003,115.430000,111.099998,111.199997,110.842850,143937800 137 | 2020-10-29,112.370003,116.930000,112.199997,115.320000,114.949615,146129200 138 | 2020-10-30,111.059998,111.989998,107.720001,108.860001,108.510361,190272600 139 | 2020-11-02,109.110001,110.680000,107.320000,108.769997,108.420654,122866900 140 | 2020-11-03,109.660004,111.489998,108.730003,110.440002,110.085289,107624400 141 | 2020-11-04,114.139999,115.589996,112.349998,114.949997,114.580803,138235500 142 | 2020-11-05,117.949997,119.620003,116.870003,119.029999,118.647697,126387100 143 | 2020-11-06,118.320000,119.199997,116.129997,118.690002,118.512909,114457900 144 | 2020-11-09,120.500000,121.989998,116.050003,116.320000,116.146439,154515300 145 | 2020-11-10,115.550003,117.589996,114.129997,115.970001,115.796967,138023400 146 | 2020-11-11,117.190002,119.629997,116.440002,119.489998,119.311707,112295000 147 | 2020-11-12,119.620003,120.529999,118.570000,119.209999,119.032127,103162300 148 | 2020-11-13,119.440002,119.669998,117.870003,119.260002,119.082054,81581900 149 | 2020-11-16,118.919998,120.989998,118.150002,120.300003,120.120506,91183000 150 | 2020-11-17,119.550003,120.669998,118.959999,119.389999,119.211861,74271000 151 | 2020-11-18,118.610001,119.820000,118.000000,118.029999,117.853889,76322100 152 | 2020-11-19,117.589996,119.059998,116.809998,118.639999,118.462982,74113000 153 | 2020-11-20,118.639999,118.769997,117.290001,117.339996,117.164917,73604300 154 | 2020-11-23,117.180000,117.620003,113.750000,113.849998,113.680122,127959300 155 | 2020-11-24,113.910004,115.849998,112.589996,115.169998,114.998154,113874200 156 | 2020-11-25,115.550003,116.750000,115.169998,116.029999,115.856873,76499200 157 | 2020-11-27,116.570000,117.489998,116.220001,116.589996,116.416031,46691300 158 | 2020-11-30,116.970001,120.970001,116.809998,119.050003,118.872368,169410200 159 | 2020-12-01,121.010002,123.470001,120.010002,122.720001,122.536896,128166800 160 | 2020-12-02,122.019997,123.370003,120.889999,123.080002,122.896355,89004200 161 | 2020-12-03,123.519997,123.779999,122.209999,122.940002,122.756569,78967600 162 | 2020-12-04,122.599998,122.860001,121.519997,122.250000,122.067596,78260400 163 | 2020-12-07,122.309998,124.570000,122.250000,123.750000,123.565353,86712000 164 | 2020-12-08,124.370003,124.980003,123.089996,124.379997,124.194412,82225500 165 | 2020-12-09,124.529999,125.949997,121.000000,121.779999,121.598289,115089200 166 | 2020-12-10,120.500000,123.870003,120.150002,123.239998,123.056114,81312200 167 | 2020-12-11,122.430000,122.760002,120.550003,122.410004,122.227356,86939800 168 | 2020-12-14,122.599998,123.349998,121.540001,121.779999,121.598289,79184500 169 | 2020-12-15,124.339996,127.900002,124.129997,127.879997,127.689186,157572300 170 | 2020-12-16,127.410004,128.369995,126.559998,127.809998,127.619293,98208600 171 | 2020-12-17,128.899994,129.580002,128.039993,128.699997,128.507965,94359800 172 | 2020-12-18,128.960007,129.100006,126.120003,126.660004,126.471016,192541500 173 | 2020-12-21,125.019997,128.309998,123.449997,128.229996,128.038666,121251600 174 | 2020-12-22,131.610001,134.410004,129.649994,131.880005,131.683228,168904800 175 | 2020-12-23,132.160004,132.429993,130.779999,130.960007,130.764603,88223700 176 | 2020-12-24,131.320007,133.460007,131.100006,131.970001,131.773087,54930100 177 | 2020-12-28,133.990005,137.339996,133.509995,136.690002,136.486053,124486200 178 | 2020-12-29,138.050003,138.789993,134.339996,134.869995,134.668762,121047300 179 | 2020-12-30,135.580002,135.990005,133.399994,133.720001,133.520477,96452100 180 | 2020-12-31,134.080002,134.740005,131.720001,132.690002,132.492020,99116600 181 | 2021-01-04,133.520004,133.610001,126.760002,129.410004,129.216919,143301900 182 | 2021-01-05,128.889999,131.740005,128.429993,131.009995,130.814514,97664900 183 | 2021-01-06,127.720001,131.050003,126.379997,126.599998,126.411102,155088000 184 | 2021-01-07,128.360001,131.630005,127.860001,130.919998,130.724655,109578200 185 | 2021-01-08,132.429993,132.630005,130.229996,132.050003,131.852966,105158200 186 | 2021-01-11,129.190002,130.169998,128.500000,128.979996,128.787552,100620900 187 | 2021-01-12,128.500000,129.690002,126.860001,128.800003,128.607819,91951100 188 | 2021-01-13,128.759995,131.449997,128.490005,130.889999,130.694702,88636800 189 | 2021-01-14,130.800003,131.000000,128.759995,128.910004,128.717667,90221800 190 | 2021-01-15,128.779999,130.220001,127.000000,127.139999,126.950294,111598500 191 | 2021-01-19,127.779999,128.710007,126.940002,127.830002,127.639267,90757300 192 | 2021-01-20,128.660004,132.490005,128.550003,132.029999,131.832993,104319500 193 | 2021-01-21,133.800003,139.669998,133.589996,136.869995,136.665771,120529500 194 | 2021-01-22,136.279999,139.850006,135.020004,139.070007,138.862503,114459400 195 | 2021-01-25,143.070007,145.089996,136.539993,142.919998,142.706757,157611700 196 | 2021-01-26,143.600006,144.300003,141.369995,143.160004,142.946396,98390600 197 | 2021-01-27,143.429993,144.300003,140.410004,142.059998,141.848038,140843800 198 | 2021-01-28,139.520004,141.990005,136.699997,137.089996,136.885452,142621100 199 | 2021-01-29,135.830002,136.740005,130.210007,131.960007,131.763107,177180600 200 | 2021-02-01,133.750000,135.380005,130.929993,134.139999,133.939850,106239800 201 | 2021-02-02,135.729996,136.309998,134.610001,134.990005,134.788589,83305400 202 | 2021-02-03,135.759995,135.770004,133.610001,133.940002,133.740158,89880900 203 | 2021-02-04,136.300003,137.399994,134.589996,137.389999,137.184998,84183100 204 | 2021-02-05,137.350006,137.419998,135.860001,136.759995,136.759995,75524000 205 | 2021-02-08,136.029999,136.960007,134.919998,136.910004,136.910004,71297200 206 | 2021-02-09,136.619995,137.880005,135.850006,136.009995,136.009995,76774200 207 | 2021-02-10,136.479996,136.990005,134.399994,135.389999,135.389999,73046600 208 | 2021-02-11,135.899994,136.389999,133.770004,135.130005,135.130005,64154400 209 | 2021-02-12,134.350006,135.529999,133.690002,135.369995,135.369995,60029300 210 | 2021-02-16,135.490005,136.009995,132.789993,133.190002,133.190002,80576300 211 | 2021-02-17,131.250000,132.220001,129.470001,130.839996,130.839996,97918500 212 | 2021-02-18,129.199997,130.000000,127.410004,129.710007,129.710007,96856700 213 | 2021-02-19,130.240005,130.710007,128.800003,129.869995,129.869995,87668800 214 | 2021-02-22,128.009995,129.720001,125.599998,126.000000,126.000000,103916400 215 | 2021-02-23,123.760002,126.709999,118.389999,125.860001,125.860001,158273000 216 | 2021-02-24,124.940002,125.559998,122.230003,125.349998,125.349998,111039900 217 | 2021-02-25,124.680000,126.459999,120.540001,120.989998,120.989998,148199500 218 | 2021-02-26,122.589996,124.849998,121.199997,121.260002,121.260002,164320000 219 | 2021-03-01,123.750000,127.930000,122.790001,127.790001,127.790001,115998300 220 | 2021-03-02,128.410004,128.720001,125.010002,125.120003,125.120003,102015300 221 | 2021-03-03,124.809998,125.709999,121.839996,122.059998,122.059998,112430400 222 | 2021-03-04,121.750000,123.599998,118.620003,120.129997,120.129997,177275300 223 | 2021-03-05,120.980003,121.940002,117.570000,121.419998,121.419998,153590400 224 | 2021-03-08,120.930000,121.000000,116.209999,116.360001,116.360001,153918600 225 | 2021-03-09,119.029999,122.059998,118.790001,121.089996,121.089996,129159600 226 | 2021-03-10,121.690002,122.169998,119.449997,119.980003,119.980003,111760400 227 | 2021-03-11,122.540001,123.209999,121.260002,121.959999,121.959999,102753600 228 | 2021-03-12,120.400002,121.169998,119.160004,121.029999,121.029999,87963400 229 | 2021-03-15,121.410004,124.000000,120.419998,123.989998,123.989998,92403800 230 | 2021-03-16,125.699997,127.220001,124.720001,125.570000,125.570000,114740000 231 | 2021-03-17,124.050003,125.860001,122.339996,124.760002,124.760002,111437500 232 | 2021-03-18,122.879997,123.180000,120.320000,120.529999,120.529999,121229700 233 | 2021-03-19,119.900002,121.430000,119.680000,119.989998,119.989998,185023200 234 | 2021-03-22,120.330002,123.870003,120.260002,123.389999,123.389999,111912300 235 | 2021-03-23,123.330002,124.239998,122.139999,122.540001,122.540001,95467100 236 | 2021-03-24,122.820000,122.900002,120.070000,120.089996,120.089996,88530500 237 | 2021-03-25,119.540001,121.660004,119.000000,120.589996,120.589996,98844700 238 | 2021-03-26,120.349998,121.480003,118.919998,121.209999,121.209999,93958900 239 | 2021-03-29,121.650002,122.580002,120.730003,121.389999,121.389999,80819200 240 | 2021-03-30,120.110001,120.400002,118.860001,119.900002,119.900002,85671900 241 | 2021-03-31,121.650002,123.519997,121.150002,122.150002,122.150002,118323800 242 | 2021-04-01,123.660004,124.180000,122.489998,123.000000,123.000000,74957400 243 | 2021-04-05,123.870003,126.160004,123.070000,125.900002,125.900002,88651200 244 | 2021-04-06,126.500000,127.129997,125.650002,126.209999,126.209999,80171300 245 | 2021-04-07,125.830002,127.919998,125.139999,127.900002,127.900002,83466700 246 | 2021-04-08,128.949997,130.389999,128.520004,130.360001,130.360001,88844600 247 | 2021-04-09,129.800003,133.039993,129.470001,133.000000,133.000000,106513800 248 | 2021-04-12,132.520004,132.850006,130.630005,131.240005,131.240005,91420000 249 | 2021-04-13,132.440002,134.660004,131.929993,134.429993,134.429993,91266500 250 | 2021-04-14,134.940002,135.000000,131.660004,132.029999,132.029999,87222800 251 | 2021-04-15,133.820007,135.000000,133.639999,134.500000,134.500000,89347100 252 | 2021-04-16,134.300003,134.669998,133.279999,134.160004,134.160004,84818500 253 | 2021-04-19,133.509995,135.470001,133.339996,134.839996,134.839996,93996100 254 | 2021-04-20,135.020004,135.529999,132.281296,132.740005,132.740005,68181997 --------------------------------------------------------------------------------