├── .DS_Store ├── .babelrc ├── .dockerignore ├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── package.json ├── src ├── .DS_Store ├── actions │ ├── AccountActions.js │ ├── AuthedActions.js │ ├── NewsActions.js │ ├── OrderActions.js │ ├── PortfolioActions.js │ ├── QuoteActions.js │ ├── WatchlistActions.js │ ├── index.js │ └── positionActions.js ├── components │ ├── AboutPage.js │ ├── Account.js │ ├── AccountPane.js │ ├── App.js │ ├── History.js │ ├── LandingPage.js │ ├── Login.js │ ├── News.js │ ├── NotFoundPage.js │ ├── Order.js │ ├── PositionList.js │ ├── QuotePane.js │ ├── Root.js │ ├── Search.js │ ├── SearchResults.js │ ├── Sidebar.js │ ├── Spinner.js │ ├── Watchlist.js │ └── common │ │ └── Button.js ├── constants │ └── ActionTypes.js ├── containers │ ├── AccountPage.js │ ├── App.js │ ├── HistoryPage.js │ ├── LoginPage.js │ ├── NewsPane.js │ ├── OrderPage.js │ ├── PositionsPane.js │ ├── QuoteContainer.js │ ├── SettingsPage.js │ └── WatchlistPane.js ├── index.ejs ├── index.js ├── lib │ ├── Config.js │ ├── Schemas.js │ ├── Utils.js │ ├── formaters.js │ ├── processPortfolio.js │ └── request.js ├── middleware │ └── api.js ├── reducers │ ├── account.js │ ├── authentication.js │ ├── order.js │ ├── portfolio.js │ ├── positions.js │ ├── quote.js │ ├── rootReducer.js │ └── watchlist.js ├── routes.js ├── server.js └── store │ ├── configureStore.js │ ├── initialState.js │ └── localStorage.js ├── static ├── .DS_Store ├── favicon.ico ├── img │ ├── graph-green-lrg.svg │ ├── graph-green.svg │ ├── graph-red-lrg.svg │ ├── graph-red.svg │ └── logo.png ├── index.html └── robots.txt ├── webpack.config.js └── yarn.lock /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moosh3/robinhood-react/3c89e4016a44102d62962bbeceb43dcf333c6974/.DS_Store -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "react", "stage-0"], 3 | } 4 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Unix-style newlines with a newline ending every file 7 | [*] 8 | end_of_line = lf 9 | insert_final_newline = true 10 | 11 | # Matches multiple files with brace expansion notation 12 | # Set default charset 13 | [src/**.js] 14 | charset = utf-8 15 | indent_style = space 16 | indent_size = 2 17 | 18 | # Matches the exact files either package.json or .travis.yml 19 | [{package.json,.travis.yml}] 20 | indent_style = space 21 | indent_size = 2 22 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | coverage 4 | webpack.*.js 5 | *server.js 6 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "airbnb", 4 | "plugin:flowtype/recommended" 5 | ], 6 | "plugins": [ 7 | "flowtype" 8 | ], 9 | "env": { 10 | "browser": true, 11 | "jest": true 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | public/dist/ 3 | node_modules/ 4 | npm-debug.log 5 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:argon 2 | 3 | # Create app directory 4 | RUN mkdir -p /usr/src/app 5 | WORKDIR /usr/src/app 6 | 7 | # Install app dependencies 8 | COPY package.json /usr/src/app/ 9 | RUN yarn install 10 | 11 | # Bundle app source 12 | COPY . /usr/src/app 13 | 14 | EXPOSE 8080 15 | CMD [ "yarn", "start" ] 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Alec Cunningham 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Robinhood React 2 | 3 | ## WORK IN PROGRESS 4 | 5 | UI built by [Jason Maurer](https://github.com/jsonmaur/robinhood-web)! 6 | 7 | ![Imgur](http://i.imgur.com/JCAoxSE.png) 8 | 9 | 10 | 11 | This packages uses [Robinhoods API](https://github.com/sanko/Robinhood) allowing you to use Robinhood in your browser. Limited to (well designed!) mobile apps no more! 12 | 13 | Check out the [Trello board](https://trello.com/b/A6Kpou2w/robinhood-react) to stay up to date on the development! 14 | 15 | ## Requirements 16 | * node `^7.0.0` <-- probably works on older versions, but this is the only versions its been tested on 17 | * yarn `^0.20.3` 18 | 19 | ## Getting Started 20 | 21 | First, clone the project: 22 | 23 | ```bash 24 | $ git clone https://github.com/aleccunningham/robinhood-react robinhood 25 | $ cd robinhood 26 | ``` 27 | Then install dependencies and check to see it works. It is recommended that you use [Yarn](https://yarnpkg.com/) for deterministic installs, but `npm install` will work just as well. 28 | 29 | ```bash 30 | $ yarn install # Install project dependencies 31 | $ yarn start # Compile and launch (same as `npm start`) 32 | ``` 33 | 34 | ### Reference 35 | 36 | Below is a quick reference to the code base, mainly to give structure to the flow of the actions -> reducers -> store 37 | 38 | You can find the style guide for writing react components and their redux counterparts [in our wiki](https://github.com/aleccunningham/robinhood-react/wiki/Style-Guide). 39 | 40 | ## Helper packages 41 | 42 | - [react-notie](https://github.com/vkbansal/react-notie) 43 | - [title-decorator](https://github.com/gigobyte/react-document-title-decorator) 44 | - [reselect](https://github.com/reactjs/reselect) 45 | - [normalizr](https://github.com/paularmstrong/normalizr) 46 | - [upup](https://github.com/TalAter/UpUp) 47 | - [js-cookie](https://github.com/js-cookie/js-cookie) 48 | - [recompose](https://github.com/acdlite/recompose#composition) 49 | - babel 50 | - react 51 | - redux 52 | - react-router 53 | - webpack 54 | 55 | ## Project Structure 56 | 57 | ``` 58 | |-- .babelrc 59 | |-- .eslintrc 60 | |-- .eslintignore 61 | |-- .gitignore 62 | |-- .dockerignore 63 | |-- .editorconfig 64 | |-- Dockerfile 65 | |-- README.md 66 | |-- CHANGELOG.md 67 | |-- LICENSE 68 | |-- package.json 69 | |-- yarn.lock 70 | |-- client/ 71 | | |-> index.js 72 | | |-> routes.js 73 | |-- shared/ 74 | | |-- actions/ 75 | | |-- AccountActions.js 76 | | |-- AuthedActions.js 77 | | |-- NewsActions.js 78 | | |-- OrderActions.js 79 | | |-- PortfolioActions.js 80 | | |-- QuoteActions.js 81 | | |-- WatchlistActions.js 82 | | |-- components/ 83 | | |-- Account.js 84 | | |-- Banner.js 85 | | |-- FilterSearch.js 86 | | |-- Footer.js 87 | | |-- Link.js 88 | | |-- Nav.js 89 | | |-- NavSearch.js 90 | | |-- Quote.js 91 | | |-- Row.js 92 | | |-- Table.js 93 | | |-- containers/ 94 | | |-- AccountContainer.js 95 | | |-- App.js 96 | | |-- DashboardContainer.js 97 | | |-- LoginContainer.js 98 | | |-- NotFoundPage.js 99 | | |-- OrderContainer.js 100 | | |-- QuoteContainer.js 101 | | |-- Root.js 102 | | |-- constants/ 103 | | |-- ActionTypes.js 104 | | |-- Config.js 105 | | |-- Schemas.js 106 | | |-- reducers/ 107 | | |-- authentication.js 108 | | |-- order.js 109 | | |-- quote.js 110 | | |-- rootReducer.js 111 | | |-- user.js 112 | | |-- watchlist.js 113 | | |-- store/ 114 | | |-- configureStore.js 115 | |-- server/ 116 | | |-- index.js 117 | | |-- server.js 118 | |-- webpack.config.js 119 | ``` 120 | 121 | 122 | ```actions/``` contain all action creators. These are triggered by events and return an action object (or function, using thunk middleware) that will notify reducers there needs to be a change in the state 123 | 124 | ```components/``` contains dumb components 125 | 126 | ```containers/``` contains react components that serve as containers for multiple dumb components. This will connect React components to the Redux store. The ```connect``` method will map the state to props by subscribing to a store that has been passed through the components context and will tell the component to update whenever the state changes. ```connect``` can also map dispatch to props by connecting action creators to and to the store dispatch method allowing the props to be invoked directly (rather than ```store.dispatch({type: 'MY_ACTION', id: 1})```). 127 | 128 | ```reducers/``` hold functions that take the current state and an action type and return a new state with the changes 129 | 130 | ```store/``` allows for the definition of a custom redux store to insert middleware, in this instance ```thunk```. 131 | 132 | ### Flow 133 | 134 | ``` 135 | - Dashboard 136 | - total cash 137 | - news 138 | - positions 139 | - quotes 140 | - watchlists 141 | - Account 142 | - daytrade warnings 143 | - gold 144 | - withdrawalabe funds 145 | - buying power 146 | - pending orders 147 | - instant deposits 148 | - History 149 | - All history 150 | - Filter by buy, sell, age 151 | - Quote 152 | - Owned Quote 153 | - Quote + 154 | - shares 155 | - equity value 156 | - average cost 157 | - total return 158 | - todays return 159 | - orders 160 | - Banking 161 | - linked accounts 162 | - transfer to robinhood 163 | - transfer to bank 164 | ``` 165 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "js-stack-from-stratch", 3 | "version": "1.0.0", 4 | "description": "Javascript app framework stack", 5 | "main": "index.js", 6 | "author": "Alec Cunningham", 7 | "license": "MIT", 8 | "scripts": { 9 | "start": "yarn dev:start", 10 | "dev:start": "nodemon -e js,jsx --ignore lib --ignore dist --exec babel-node src/server", 11 | "dev:wds": "webpack-dev-server --progress", 12 | "prod:build": "rimraf lib && babel src -d lib --ignore .test.js && cross-env NODE_ENV=production webpack -p --progress", 13 | "prod:start": "cross-env NODE_ENV=production pm2 start lib/server && pm2 logs", 14 | "prod:stop": "pm2 delete all", 15 | "lint": "eslint src webpack.config.js --ext .js,.jsx", 16 | "test": "eslint src && flow && jest --coverage", 17 | "compile": "babel" 18 | }, 19 | "dependencies": { 20 | "babel-polyfill": "^6.23.0", 21 | "compression": "^1.6.2", 22 | "express": "^4.15.0", 23 | "immutable": "^3.8.1", 24 | "js-cookie": "^2.1.3", 25 | "lodash": "^4.17.4", 26 | "moment": "^2.17.1", 27 | "normalizr": "^3.2.2", 28 | "numeral": "^2.0.4", 29 | "react": "^15.4.2", 30 | "react-dom": "^15.4.2", 31 | "react-hot-loader": "next", 32 | "react-pure-render": "^1.0.2", 33 | "react-redux": "^5.0.3", 34 | "react-router": "next", 35 | "react-router-dom": "next", 36 | "react-router-redux": "^4.0.8", 37 | "react-search-input": "^0.10.4", 38 | "redux": "^3.6.0", 39 | "redux-actions": "^2.0.1", 40 | "redux-thunk": "^2.2.0", 41 | "webpack-dev-middleware": "^1.10.1", 42 | "webpack-hot-middleware": "^2.17.1" 43 | }, 44 | "devDependencies": { 45 | "babel-cli": "^6.24.0", 46 | "babel-core": "^6.23.1", 47 | "babel-eslint": "^7.1.1", 48 | "babel-jest": "^19.0.0", 49 | "babel-loader": "^6.3.2", 50 | "babel-preset-flow": "^6.23.0", 51 | "babel-preset-latest": "^6.22.0", 52 | "babel-preset-react": "^6.23.0", 53 | "babel-preset-react-hmre": "^1.1.1", 54 | "cross-env": "^3.2.3", 55 | "eslint": "^3.17.0", 56 | "eslint-config-airbnb": "^14.1.0", 57 | "eslint-plugin-flowtype": "^2.30.0", 58 | "eslint-plugin-import": "^2.2.0", 59 | "eslint-plugin-jsx-a11y": "^3.0.2 || ^4.0.0", 60 | "eslint-plugin-react": "^6.9.0", 61 | "flow-bin": "^0.40.0", 62 | "html-webpack-plugin": "^2.28.0", 63 | "husky": "^0.13.2", 64 | "jest": "^19.0.2", 65 | "nodemon": "^1.11.0", 66 | "pm2": "^2.4.2", 67 | "rimraf": "^2.6.1", 68 | "webpack": "^2.2.1", 69 | "webpack-dev-server": "^2.4.1" 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moosh3/robinhood-react/3c89e4016a44102d62962bbeceb43dcf333c6974/src/.DS_Store -------------------------------------------------------------------------------- /src/actions/AccountActions.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import * as types from '../constants/ActionTypes'; 3 | import { checkStatus, accountIdUrl, accountInfoUrl } from '../shared/utils'; 4 | 5 | /* //////////////////////////////// 6 | // Account Actions // 7 | ////////////////////////////////*/ 8 | /* 9 | AccountID 10 | - *Needs authToken* 11 | - URI: api.robinhood.com/user/id/ 12 | - Method: GET 13 | - Sample Response: 14 | { 15 | "username": "superman", 16 | "url": "https://api.robinhood.com/user/id/", 17 | "id": "11deface-face-face-face-defacedeface11" 18 | } 19 | 20 | AccountInfo 21 | - *Needs authToken* 22 | - URI: api.robinhood.com/user/basic_info/ 23 | - Method: GET 24 | - Sample Response: 25 | { 26 | "phone_number": "2125550030", 27 | "city": "New York", 28 | "number_dependents": 2, 29 | "citizenship": "US", 30 | "updated_at": "2016-03-13T12:18:02.820164Z", 31 | "marital_status": "married", 32 | "zipcode": "10001", 33 | "country_of_residence": "US", 34 | "state": "NY", 35 | "date_of_birth": "1978-12-18", 36 | "user": "https://api.robinhood.com/user/", 37 | "address": "320 10th Av", 38 | "tax_id_ssn": "0001" 39 | } 40 | */ 41 | 42 | function requestAccountID(authToken) { 43 | return { 44 | type: types.REQUEST_ACCOUNT_ID, 45 | authToken, 46 | }; 47 | } 48 | 49 | function recieveAccountID(response) { 50 | return { 51 | type: types.RECIEVE_ACCOUNT_ID, 52 | response, 53 | recievedAt: Date.now(), 54 | }; 55 | } 56 | 57 | function fetchAccountID(authToken) { 58 | return dispatch => { 59 | dispatch(requestAccountID(authToken)) 60 | return fetch(accountIdUrl(), { 61 | method: 'GET', 62 | headers: { 63 | 'Accept': 'application/json, */*', 64 | 'Content-Type': 'application/json', 65 | 'Authorization': `Token ${authToken}` 66 | }) 67 | .then(checkStatus) 68 | .then(response => response.json()) 69 | .then(json => dispatch(recieveAccountID(json))) 70 | }; 71 | } 72 | 73 | function shouldFetchAccountID(state, authToken) { 74 | const id = state.account.id; 75 | if (_.isEmpty(id)) { 76 | return true; 77 | } 78 | if (id.isFetching) { 79 | return false; 80 | } 81 | return true; 82 | } 83 | 84 | export function fetchAccountIdIfNeeded(authToken) { 85 | return (dispatch, getState) => { 86 | if (shouldFetchAccountID(getState(), authToken)) { 87 | return dispatch(fetchAccountID(authToken)) 88 | } 89 | }; 90 | } 91 | 92 | function requestAccountInfo(authToken) { 93 | return: { 94 | type: types.REQUEST_ACCOUNT_INFO, 95 | authToken 96 | }; 97 | } 98 | 99 | function recieveAccountInfo(response) { 100 | return: { 101 | type: types.RECIEVE_ACCOUNT_INFO, 102 | response 103 | recievedAt: Date.now() 104 | }; 105 | } 106 | 107 | function fetchAccountInfo(authToken) { 108 | return dispatch => { 109 | dispatch(requestAccountInfo(authToken)) 110 | return fetch(accountInfoUrl(), { 111 | method: 'GET', 112 | headers: { 113 | 'Accept': 'application/json, */*', 114 | 'Content-Type': 'application/json', 115 | 'Authorization': `Token ${authToken}` 116 | }) 117 | .then(checkStatus) 118 | .then(response => response.json()) 119 | .then(json => dispatch(recieveAccountInfo(json))) 120 | } 121 | } 122 | 123 | function shouldFetchAccountInfo(state, authToken) { 124 | const info = state.account.info; 125 | 126 | if (_.isEmpty(info)) { 127 | return true; 128 | } 129 | if (accountInfo.isFetching) { 130 | return false; 131 | } 132 | return fetchAccountInfo(authToken); 133 | } 134 | 135 | export function fetchAccountInfoIfNeeded(authToken) { 136 | return (dispatch, getState) => { 137 | if (shouldFetchAccountInfo(getState(), authToken)) { 138 | return dispatch(fetchAccountInfo(authToken)) 139 | } 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/actions/AuthedActions.js: -------------------------------------------------------------------------------- 1 | import * as types from '../constants/ActionTypes'; 2 | import { checkStatus } from '../shared/Utils'; 3 | import { fetchWatchlists } from './WatchlistActions'; 4 | import { fetchPortfolio, fetchPositions } from './PortfolioActions'; 5 | import { fetchAccountID, fetchAccountInfo } from './AccountActions'; 6 | 7 | /* //////////////////////////////// 8 | // Authentication // 9 | /////////////////////////////////*/ 10 | /* 11 | fetchAuthedUser 12 | - *Needs authToken* 13 | - URI: https://api.robinhood.com/user/ 14 | - Sample response: 15 | { 16 | "username": "superman", 17 | "first_name": "Clark", 18 | "last_name": "Kent", 19 | "id_info": "https://api.robinhood.com/user/id/", 20 | "url": "https://api.robinhood.com/user/", 21 | "basic_info": "https://api.robinhood.com/user/basic_info/", 22 | "email": "s@itmeanshope.com", 23 | "investment_profile": "https://api.robinhood.com/user/investment_profile/", 24 | "id": "11deface-face-face-face-defacedeface11", 25 | "international_info": "https://api.robinhood.com/user/international_info/", 26 | "employment": "https://api.robinhood.com/user/employment/", 27 | "additional_info": "https://api.robinhood.com/user/additional_info/" 28 | } 29 | */ 30 | 31 | const COOKIE_PATH = 'authToken'; 32 | 33 | function authUser(authToken) { 34 | return dispatch => 35 | dispatch(fetchAuthedUser(authToken)); 36 | } 37 | 38 | function loginUser(credentials) { 39 | let url = apiUrl + endpoints['login']; 40 | let form = new FormData(document.getElementById('login-form')); 41 | return dispatch => { 42 | dispatch(loginAttempt(credentials)) 43 | return fetch(url, { 44 | method: 'POST', 45 | headers: { 46 | 'Accept': 'application/json, */*', 47 | 'Content-Type': 'application/json', 48 | }, 49 | body: JSON.stringify({'username': {username}, 'password': {password}), 50 | }) 51 | .then(checkStatus) 52 | .then(response => response.json()) 53 | .then(json[0] => dispatch(loginSuccessPre(authToken))) 54 | }; 55 | } 56 | 57 | function loginSuccessPre(authToken) { 58 | return dispatch => { 59 | dispatch(authUser(authToken)); 60 | dispatch(fetchAccountInfo(authToken)); 61 | dispatch(fetchAccountID(authToken)); 62 | dispatch(fetchWatchlists(authToken)); 63 | dispatch(fetchPortfolio(authToken)); 64 | dispatch(fetchPositions(authToken)); 65 | } 66 | } 67 | 68 | function logoutUser() { 69 | return (dispatch, getState) => { 70 | Cookies.remove(COOKIE_PATH); 71 | const { authed } = getState(); 72 | return dispatch(resetAuthed(authToken)); 73 | } 74 | } 75 | 76 | function resetAuthed(authToken) { 77 | // Hits the logout endpoint 78 | return (dispatch, state) => { 79 | 80 | } 81 | } 82 | 83 | function initAuth() { 84 | return dispatch => { 85 | const authToken = Cookies.get(COOKIE_PATH); 86 | if (authToken) { 87 | return dispatch(authUser(authToken)); 88 | } 89 | return null; 90 | }; 91 | } 92 | 93 | function requestAuth(credentials) { 94 | return { 95 | type: types.REQUEST_AUTH, 96 | credentials, 97 | }; 98 | } 99 | 100 | function recieveAuth(authToken) { 101 | return { 102 | type: types.RECIEVE_AUTH, 103 | authToken, 104 | }; 105 | } 106 | 107 | function receiveAccessToken(accessToken) { 108 | return { 109 | type: types.RECEIVE_ACCESS_TOKEN, 110 | accessToken, 111 | }; 112 | } 113 | 114 | function authFailure(error) { 115 | return { 116 | type: types.AUTH_FAILURE, 117 | error, 118 | }; 119 | } 120 | 121 | /* //////////////////////////////// 122 | // Robinhood Gold // 123 | /////////////////////////////////*/ 124 | 125 | function fetchVolatility(equity) { 126 | return { 127 | type: types.FETCH_RG_VOLATILITY, 128 | equity 129 | }; 130 | } 131 | 132 | function fetchInitialRequirements(equity) { 133 | return { 134 | type: types.FETCH_RG_INITIAL_REQUIREMENTS, 135 | equity, 136 | }; 137 | } 138 | 139 | function fetchMaintenance(equity) { 140 | return { 141 | type: types.FETCH_RG_MAINTENANCE, 142 | equity 143 | }; 144 | } 145 | -------------------------------------------------------------------------------- /src/actions/NewsActions.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import * as types from '../constants/ActionTypes'; 3 | import { newsUrl, topMoversUrl } from '../lib/utils'; 4 | // Get Popular Stocks GET https://brokerage-static.s3.amazonaws.com/popular_stocks/data.json 5 | 6 | // Get Top Movers on the S&P500 GET /midlands/movers/sp500/?direction=['up' or 'down'] 7 | 8 | /* //////////////////////////////// 9 | // Quote News // 10 | /////////////////////////////////*/ 11 | 12 | export const requestNews = (symbol) => ({ 13 | type: types.REQUEST_NEWS, 14 | symbol, 15 | }) 16 | 17 | export const receieveNews = (instrument, json) => ({ 18 | type: types.RECIEVE_NEWS, 19 | data: json.data.children.map(child => child.data), 20 | instrument, 21 | recievedAt: Date.now(), 22 | }) 23 | 24 | export const requestNewsError = (bool) => ({ 25 | type: types.REQUEST_NEWS_ERROR, 26 | error: bool, 27 | }) 28 | 29 | function fetchNews(instrument) { 30 | return dispatch => { 31 | dispatch(requestNews(instrument)); // Show loading spinner 32 | 33 | return fetch(newsUrl(instrument)) 34 | .then(response => response.json()) 35 | .then(json => { 36 | dispatch(recieveQuote(symbol, json)); 37 | }) 38 | .then(checkStatus) 39 | .catch(error => { 40 | dispatch(quoteRequestError(true)) 41 | }) 42 | }; 43 | } 44 | 45 | function shouledFetchNews(instrument) { 46 | const news = state.news; 47 | if (!news) { 48 | return true 49 | } 50 | if (news.isFetching) { 51 | return false 52 | } 53 | return dispatch(fetchNews(instrument)); 54 | } 55 | 56 | export function fetchNewsIfNeeded(instrument) { 57 | return (dispatch, getState) => { 58 | if (shouledFetchNews(getState(), instrument)) { 59 | return dispatch(fetchNews(instrument)) 60 | } 61 | } 62 | } 63 | 64 | /* //////////////////////////////// 65 | // Top movers // 66 | /////////////////////////////////*/ 67 | 68 | export const requestMovers = (direction) => ({ 69 | type: types.REQUEST_MOVERS, direction, 70 | }) 71 | 72 | export const recieveMovers = (direction, json) => ({ 73 | type: types.RECIEVE_MOVERS, 74 | data: json.data.children.map(child => child.data), 75 | direction, 76 | recievedAt: Date.now(), 77 | }) 78 | 79 | export const requestMoversError = (bool) => ({ 80 | type: types.REQUEST_MOVERS_ERROR, 81 | error: bool, 82 | }) 83 | 84 | function fetchNews(direction) { 85 | return dispatch => { 86 | dispatch(requestNews(instrument)); // Show loading spinner 87 | 88 | return fetch(moversUrl(direction)) 89 | .then(response => response.json()) 90 | .then(json => { 91 | dispatch(recieverMovers(symbol, json)); 92 | }) 93 | .then(checkStatus) 94 | .catch(error => { 95 | dispatch(moversRequestError(true)) 96 | }) 97 | }; 98 | } 99 | 100 | function shouldFetchMovers(direction) { 101 | const movers = state.movers; 102 | if (!movers) { 103 | return true 104 | } 105 | if (movers.isFetching) { 106 | return false 107 | } 108 | return dispatch(fetchMovers(instrument)); 109 | } 110 | 111 | export function fetchMoversIfNeeded(direction) { 112 | return (dispatch, getState) => { 113 | if (shouldFetchMovers(getState(), direction)) { 114 | return dispatch(fetchMovers(direction)) 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/actions/OrderActions.js: -------------------------------------------------------------------------------- 1 | import * as types from '../constants/ActionTypes'; 2 | 3 | /* //////////////////////////////// 4 | // Placing Order // 5 | ////////////////////////////////*/ 6 | /* 7 | - URI api.robinhood.com/orders/ 8 | - Fields = [ 9 | - account 10 | - instrument 11 | - symbol 12 | - type 13 | - time_in_force 14 | - trigger 15 | - price 16 | - stop_price 17 | - quantity 18 | - side 19 | - client_id 20 | ] 21 | - Response sample 22 | { 23 | "updated_at": "2016-04-01T21:24:13.698563Z", 24 | "executions": [], 25 | "time_in_force": "fok", 26 | "fees": "0.00", 27 | "cancel": "https://api.robinhood.com/orders/15390ade-face-caca-0987-9fdac5824701/cancel/", 28 | "id": "15390ade-face-caca-0987-9fdac5824701", 29 | "cumulative_quantity": "0.00000", 30 | "stop_price": null, 31 | "reject_reason": null, 32 | "instrument": "https://api.robinhood.com/instruments/50810c35-d215-4866-9758-0ada4ac79ffa/", 33 | "state": "queued", 34 | "trigger": "immediate", 35 | "type": "market", 36 | "last_transaction_at": "2016-04-01T23:34:54.237390Z", 37 | "price": null, 38 | "client_id": null, 39 | "account": "https://api.robinhood.com/accounts/8UD09348/", 40 | "url": "https://api.robinhood.com/orders/15390ade-face-caca-0987-9fdac5824701/", 41 | "created_at": "2016-04-01T22:12:14.890283Z", 42 | "side": "sell", 43 | "position": "https://api.robinhood.com/positions/8UD09348/50810c35-d215-4866-9758-0ada4ac79ffa/", 44 | "average_price": null, 45 | "quantity": "1.00000" 46 | } 47 | */ 48 | 49 | export function placeOrder(authToken, order) { 50 | return { 51 | type: types.PLACE_ORDER, order 52 | }; 53 | } 54 | 55 | export function placeBuyOrder(authToken, order) { 56 | return { 57 | type: types.PLACE_BUY_ORDER, order 58 | }; 59 | } 60 | 61 | export function placeSellOrder(authToken, order) { 62 | return { 63 | type: types.PLACE_SELL_ORDER, order 64 | }; 65 | } 66 | 67 | export function gatherRecentOrders(authToken) { 68 | return { 69 | type: types.GATHER_RECENT_ORDERS, authToken 70 | }; 71 | } 72 | 73 | export function gatherOrderInfo(orderId, authToken) { 74 | return { 75 | type: types.GATHER_ORDER_INFO, orderId, authToken 76 | }; 77 | } 78 | 79 | export function cancelOrder(authToken, order) { 80 | return { 81 | type: types.CANCEL_ORDER, order 82 | }; 83 | } 84 | 85 | //////////////////////////////////////////////////////////////////////// 86 | 87 | export const backToOrderPlacementPane = () => { 88 | return { 89 | type: 'BACK_TO_ORDER_PLACEMENT_PANE' 90 | }; 91 | } 92 | export const updateCurrentOrder = (options={}) => { 93 | return { 94 | type: 'UPDATE_CURRENT_ORDER', 95 | options 96 | }; 97 | } 98 | export const resetCurrentOrder = (options={}) => { 99 | return { 100 | type: 'RESET_CURRENT_ORDER', 101 | options 102 | }; 103 | } 104 | export function selectedOrderSide(fixedTitle, options={}) { 105 | return (dispatch) => { 106 | dispatch(resetCurrentOrder({ 107 | type: options.stockType, 108 | symbol: options.symbol 109 | })); 110 | dispatch(initTitle(fixedTitle, options)); 111 | return Promise.resolve() 112 | }; 113 | } 114 | export function selectedOrderType(fixedTitle, options={}) { 115 | return (dispatch) => { 116 | dispatch(updateCurrentOrder({ 117 | type: options.stockType, 118 | })); 119 | dispatch(backToOrderPlacementPane()); 120 | return Promise.resolve() 121 | }; 122 | } 123 | export function selectedOrderTypeWithPrice(fixedTitle, options={}) { 124 | let currentOrderOptions = {type: options.stockType}; 125 | 126 | if (currentOrderOptions.type == 'stop loss') { 127 | currentOrderOptions.type = 'market'; 128 | currentOrderOptions.trigger = 'stop'; 129 | } else if (currentOrderOptions.type == 'stop limit') { 130 | currentOrderOptions.type = 'limit'; 131 | currentOrderOptions.trigger = 'stop' 132 | } 133 | return (dispatch) => { 134 | dispatch(updateCurrentOrder(currentOrderOptions)); 135 | dispatch(changeTitle(fixedTitle, options)); 136 | return Promise.resolve() 137 | }; 138 | } 139 | export function setOrderPrice(fixedTitle, options={}) { 140 | return (dispatch) => { 141 | dispatch(updateCurrentOrder({ 142 | price: options.price, 143 | })); 144 | dispatch(changeTitle(fixedTitle, options)); 145 | return Promise.resolve() 146 | }; 147 | } 148 | export function selectedTimeInForce(time_in_force) { 149 | return (dispatch) => { 150 | dispatch(updateCurrentOrder({ 151 | time_in_force, 152 | })); 153 | dispatch(backToOrderPlacementPane()); 154 | return Promise.resolve() 155 | }; 156 | } 157 | -------------------------------------------------------------------------------- /src/actions/PortfolioActions.js: -------------------------------------------------------------------------------- 1 | import * as types from '../constants/ActionTypes'; 2 | import { checkStatus } from '../shared/Utils'; 3 | 4 | /* //////////////////////////////// 5 | // Portfolio Actions // 6 | ////////////////////////////////*/ 7 | /* 8 | - *Needs authToken* 9 | - URI: api.robinhood.com/portfolios/ 10 | - Sample Response: 11 | { 12 | "results": [ 13 | { 14 | "account": "https://api.robinhood.com/accounts/{accountId}/", 15 | "adjusted_equity_previous_close": "2088.3700", 16 | "equity": "2054.9574", 17 | "equity_previous_close": "1566.3700", 18 | "excess_maintenance": "1325.3456", 19 | "excess_maintenance_with_uncleared_deposits": "1392.3456", 20 | "excess_margin": "662.7337", 21 | "excess_margin_with_uncleared_deposits": "729.7337", 22 | "extended_hours_equity": null, 23 | "extended_hours_market_value": null, 24 | "last_core_equity": "2054.9574", 25 | "last_core_market_value": "2650.4474", 26 | "market_value": "2650.4474", 27 | "start_date": "2015-08-27", 28 | "unwithdrawable_deposits": "0.0000", 29 | "unwithdrawable_grants": "0.0000", 30 | "url": "https://api.robinhood.com/portfolios/5QT32045/", 31 | "withdrawable_amount": "0.0000" 32 | } 33 | ] 34 | } 35 | */ 36 | 37 | export function requestPortfolio(authToken) { 38 | return { 39 | type: types.REQUEST_PORTFOLIO, 40 | authToken 41 | recievedAt: Date.now() 42 | }; 43 | } 44 | 45 | export function recievePortfolio(response) { 46 | return { 47 | type: types.RECIEVE_PORTFOLIO, 48 | response, 49 | recievedAt: Date.now() 50 | }; 51 | } 52 | 53 | export function requestPortfolioError() { 54 | return { 55 | type: types.REQUEST_PORTFOLIO_ERROR, 56 | recievedAt: Date.now() 57 | }; 58 | } 59 | 60 | function fetchPortfolio(authToken) { 61 | let url = apiUrl + endpoints[portfolio]; 62 | return dispatch => { 63 | fetch(url, { 64 | method: 'GET', 65 | headers: { 66 | 'Accept': 'application/json, */*', 67 | 'Content-Type': 'application/json', 68 | 'Authorization': `Token ${authToken}` 69 | }, 70 | }) 71 | .then(checkStatus) 72 | .then(response => response.json()) 73 | .then(dispatch(recievePortfolio(json))) 74 | }; 75 | } 76 | 77 | function shouldFetchPortfolio(state) { 78 | const portfolio = state.account.portfolio; 79 | 80 | if (_.isEmpty(portfolio)) { 81 | return true; 82 | } 83 | if (portfolio.isFetching) { 84 | return false; 85 | } else { 86 | return portfolio.didInvalidate; 87 | } 88 | } 89 | 90 | export function fetchPortfolioIfNeeded(authToken) { 91 | return (dispatch, getState) => { 92 | if (shouldFetchPortfolio(getState(), authToken)) { 93 | return dispatch(fetchPortfolio(authToken)) 94 | } 95 | } 96 | } 97 | 98 | /* //////////////////////////////// 99 | // Positions Actions // 100 | ////////////////////////////////*/ 101 | /* 102 | - *Needs authToken* 103 | - URI: api.robinhood.com/acount/{accountId}/positions/ 104 | - Method: GET 105 | - Sample Response: 106 | { 107 | "next": null, 108 | "previous": null, 109 | "results": [ 110 | { 111 | "account": "https://api.robinhood.com/accounts/{accountId}/", 112 | "average_buy_price": "47.7500", 113 | "created_at": "2017-03-14T15:38:58.448364Z", 114 | "instrument": "https://api.robinhood.com/instruments/bd444de7-b34a-4685-ab58-63e808f4fa16/", 115 | "intraday_average_buy_price": "47.7500", 116 | "intraday_quantity": "13.0000", 117 | "quantity": "13.0000", 118 | "shares_held_for_buys": "0.0000", 119 | "shares_held_for_sells": "0.0000", 120 | "updated_at": "2017-03-14T16:12:14.661447Z", 121 | "url": "https://api.robinhood.com/accounts/{accountID}/positions/bd444de7-b34a-4685-ab58-63e808f4fa16/" 122 | }, 123 | { 124 | "account": "https://api.robinhood.com/accounts/{accountId}/", 125 | "average_buy_price": "53.2000", 126 | "created_at": "2017-03-13T21:48:08.819064Z", 127 | "instrument": "https://api.robinhood.com/instruments/5dc3bb24-007b-459d-be90-35905a8ba8f0/", 128 | "intraday_average_buy_price": "53.2000", 129 | "intraday_quantity": "5.0000", 130 | "quantity": "5.0000", 131 | "shares_held_for_buys": "0.0000", 132 | "shares_held_for_sells": "0.0000", 133 | "updated_at": "2017-03-14T13:01:31.881251Z", 134 | "url": "https://api.robinhood.com/accounts/{accountId}/positions/5dc3bb24-007b-459d-be90-35905a8ba8f0/" 135 | }, 136 | ] 137 | } 138 | */ 139 | 140 | function requestPositions(authToken) { 141 | return: { 142 | type: types.REQUEST_POSITIONS, 143 | authToken 144 | }; 145 | } 146 | 147 | function recievePositions(json) { 148 | return: { 149 | type: types.RECIEVE_POSITIONS, 150 | positions: json, 151 | recievedAt: Date.now() 152 | }; 153 | } 154 | 155 | function fetchPositions(authToken) { 156 | return dispatch => { 157 | dispatch(requestPositions(authToken)) 158 | return fetch(url, { 159 | method: 'GET', 160 | headers: { 161 | 'Accept': 'application/json, */*', 162 | 'Content-Type': 'application/json', 163 | 'Authorization': `Token ${authToken}` 164 | }) 165 | .then(response => response.json()) 166 | .then(json => dispatch(recievePositions(json))) 167 | } 168 | } 169 | 170 | function shouldFetchPositions(state) { 171 | const positions = state.positions; 172 | 173 | if (_.isEmpty(positions)) { 174 | return true; 175 | } 176 | if (positions.isFetching) { 177 | return false; 178 | } else { 179 | return positions.didInvalidate; 180 | } 181 | } 182 | 183 | export function fetchPositionsIfNeeded(authToken) { 184 | return (dispatch, getState) => { 185 | if (shouldFetchPositions(getState(), authToken)) { 186 | return dispatch(fetchPositions(authToken)) 187 | } 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /src/actions/QuoteActions.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import * as types from '../constants/ActionTypes'; 3 | import { quoteUrl, checkStatus } from '../shared/utils'; 4 | 5 | /* //////////////////////////////// 6 | // Quote Data // 7 | /////////////////////////////////*/ 8 | /* 9 | - URI api.robinhood.com/quotes/{symbol}/ 10 | - Method: GET 11 | - Sample response 12 | { 13 | "ask_price": "54.2100", 14 | "ask_size": 2000, 15 | "bid_price": "54.2000", 16 | "bid_size": 1800, 17 | "last_trade_price": "54.1900", 18 | "last_extended_hours_trade_price": null, 19 | "previous_close": "54.6600", 20 | "adjusted_previous_close" : "54.6600", 21 | "previous_close_date": "2016-03-17", 22 | "symbol": "MSFT", 23 | "trading_halted": false, 24 | "updated_at": "2016-03-18T15:45:28Z" 25 | } 26 | 27 | Quote Fundamentals 28 | - URI api.robinhood.com/fundamentals/{symbol}/ 29 | - Sample response 30 | { 31 | "open": "63.9100", 32 | "high": "64.2800", 33 | "low": "63.6200", 34 | "volume": "8527495.0000", 35 | "average_volume": "27741172.4024", 36 | "high_52_weeks": "65.9100", 37 | "dividend_yield": "2.4287", 38 | "low_52_weeks": "48.0350", 39 | "market_cap": "496652500000.0000", 40 | "pe_ratio": "30.2971", 41 | "description": "Microsoft Corp. engages in the provision of developing and marketing software and hardware services. Its products include operating systems for computing devices, servers, phones and intelligent devices. It also offers server applications for distributed computing environments, productivity applications, business solution applications, desktop and server management tools, software development tools, video games, and online advertising. It operates through the following segments: Productivity and Business Processes, Intelligent Cloud, and More Personal Computing. The Productivity and Business Processes segment consists of products and cloud services in portfolio of productivity, communication, and information services. It comprises of office commercial, office consumer, and microsoft dynamics business solutions. The Intelligent Cloud segment offers hybrid server products and cloud services. It comprises of server products and cloud services and enterprise services. The More Personal Computing segment comprises of windows, devices, gaming, and search advertising. The company was founded by William Henry Gates III in 1975 and is headquartered in Redmond, WA.", 42 | "instrument": "https://api.robinhood.com/instruments/50810c35-d215-4866-9758-0ada4ac79ffa/" 43 | } 44 | */ 45 | 46 | export function requestQuote(symbol) { 47 | return: {type: types.REQUEST_QUOTE, symbol}; 48 | } 49 | 50 | export function recieveQuote(symbol, json) { 51 | return { 52 | type: types.RECIEVE_QUOTE, 53 | data: json.data.children.map(child => child.data)}; 54 | symbol: Object.assign({}, symbol, { symbol: json.symbol }), 55 | recievedAt: Date.now() 56 | } 57 | 58 | export function requestQuoteError(bool) { 59 | return { 60 | type: types.REQUEST_QUOTE_ERROR, 61 | error: bool 62 | }; 63 | } 64 | 65 | function fetchQuote(symbol) { 66 | return dispatch => { 67 | dispatch(requestQuote(symbol)); // Show loading spinner 68 | 69 | return fetch(constructQuoteUrl(symbol)) 70 | .then(response => response.json()) 71 | .then(json => { 72 | dispatch(recieveQuote(symbol, json)); 73 | }) 74 | .catch(error => { 75 | dispatch(quoteRequestError(true)) 76 | }) 77 | }; 78 | } 79 | 80 | function shouldFetchQuote(symbol) { 81 | const quotes = state.quote[symbol]; 82 | if (!quotes) { 83 | return true 84 | } 85 | if (quotes.isFetching) { 86 | return false 87 | } 88 | return dispatch(fetchQuote(symbol)); 89 | } 90 | 91 | export function fetchQuoteDataIfNeeded(symbol) { 92 | return (dispatch, getState) => { 93 | if (shouldFetchQuote(getState(), symbol)) { 94 | return dispatch(fetchQuote(symbol)) 95 | } 96 | } 97 | } 98 | 99 | /* //////////////////////////////// 100 | // Quote fundamentals // 101 | /////////////////////////////////*/ 102 | -------------------------------------------------------------------------------- /src/actions/WatchlistActions.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import { apiUrl, endpoints } from '../constants/Robin'; 3 | import * as types from '../constants/ActionTypes'; 4 | import { checkStatus } from '../shared/Utils'; 5 | 6 | /* //////////////////////////////// 7 | // Watchlist Actions // 8 | ////////////////////////////////*/ 9 | /* 10 | - *Needs authToken* 11 | - URI: api.robinhood.com/watchlists/ 12 | - Method: GET 13 | - Sample Response: 14 | { 15 | "next": null, 16 | "previous": null, 17 | "results": [ 18 | { 19 | "name": "Default", 20 | "url": "https://api.robinhood.com/watchlists/Default/", 21 | "user": "https://api.robinhood.com/user/" 22 | } 23 | ] 24 | } 25 | 26 | createWatchlist 27 | - *Needs authToken* 28 | - URI: api.robinhood.com/watchlists/ 29 | - Method: POST 30 | - Fields = [ 31 | - name 32 | ] 33 | - Sample response 34 | { 35 | "url": "https://api.robinhood.com/watchlists/Technology/", 36 | "user": "https://api.robinhood.com/user/", 37 | "name": "Technology" 38 | } 39 | 40 | addBulkInstrumentWatchlist 41 | - *Needs authToken* 42 | - URI: api.robinhood.com/watchlists/{watchlist}/bulk_add/ 43 | - Method: POST 44 | - Fields = [ 45 | - symbols 46 | ] 47 | - Sample response 48 | [{ 49 | "watchlist": "https://api.robinhood.com/watchlists/Default/", 50 | "instrument": "https://api.robinhood.com/instruments/50810c35-d215-4866-9758-0ada4ac79ffa/", 51 | "created_at": "2016-02-09T00:15:20.103927Z", 52 | "url": "https://api.robinhood.com/watchlists/Default/50810c35-d215-4866-9758-0ada4ac79ffa/" 53 | }, { 54 | "watchlist": "https://api.robinhood.com/watchlists/Default/", 55 | "instrument": "https://api.robinhood.com/instruments/6df56bd0-0bf2-44ab-8875-f94fd8526942/", 56 | "created_at": "2016-02-09T00:15:20.103927Z", 57 | "url": "https://api.robinhood.com/watchlists/Default/6df56bd0-0bf2-44ab-8875-f94fd8526942/" 58 | }] 59 | 60 | deleteWatchlistInstrument 61 | - *Needs authToken* 62 | - URI: api.robinhood.com/watchlists/{watchlistName}/{instrumentId}/ 63 | - Method: DELETE 64 | - Sample Response: 65 | 66 | */ 67 | 68 | export const invalidateWatchlists = (watchlists) => { 69 | return { 70 | type: types.INVALIDATE_WATCHLISTS, 71 | watchlists 72 | } 73 | } 74 | 75 | export function requestWatchlists(authToken) { 76 | return { 77 | type: types.REQUEST_WATCHLISTS, 78 | authToken 79 | }; 80 | } 81 | 82 | export function recieveWatchLists(json) { 83 | return { 84 | type: types.RECIEVE_WATCHLISTS, 85 | watchlists: json, 86 | recievedAt: Date.now() 87 | }; 88 | } 89 | 90 | function fetchWatchlists(authToken, watchlist) { 91 | return dispatch => 92 | fetch(url, { 93 | method: 'GET', 94 | headers: { 95 | 'Accept': 'application/json, */*', 96 | 'Content-Type': 'application/json', 97 | 'Authorization': `Token ${authToken}` 98 | }) 99 | .then(response => response.json()) 100 | .then(json => dispatch(recieveWatchLists(json))) 101 | } 102 | 103 | function shouldFetchWatchlists(authToken) { 104 | const watchlists = state.account.watchlists; 105 | 106 | if (_.isEmpty(watchlists)) { 107 | return true; 108 | } 109 | if (watchlists.isFetching) { 110 | return false; 111 | } else { 112 | return watchlist.didInvalidate 113 | } 114 | } 115 | 116 | export function fetchWatchlistsIfNeeded(authToken) { 117 | return (dispatch, getState) => { 118 | if (shouldFetchWatchlists(getState(), authToken)) { 119 | return dispatch(fetchWatchlists(authToken)) 120 | } 121 | } 122 | } 123 | 124 | export function createWatchlist(authToken, name) { 125 | return {type: types.CREATE_WATCHLIST, name}; 126 | } 127 | 128 | export function deleteWatchlist(authToken, name) { 129 | return {type: types.DELETE_WATCHLIST, name}; 130 | } 131 | 132 | export function addInstrumentWatchlist(authToken, symbol, watchlist) { 133 | return {type: types.ADD_TO_WATCHLIST} 134 | } 135 | 136 | export function deleteWatchlistInstrument(symbol, watchlist) { 137 | return {type: types.DELETE_FROM_WATCHLIST, watchlist}; 138 | } 139 | 140 | export function addBulkInstrumentWatchlist(authToken, symbols, watchlist) { 141 | return { 142 | fetch(constructWatchlistAddUrl(watchlist), { 143 | method: 'POST', 144 | headers: { 145 | 'Accept': 'application/json, */*', 146 | 'Content-Type': 'application/json', 147 | 'Authorization': `Token ${authToken}`, 148 | }, 149 | body: JSON.stringify({'symbols': {symbols}) 150 | }) 151 | .then(res => res.json()) 152 | .then(dispatch //TODO 153 | .catch(err => console.log('Fetch Error :-S', err)) 154 | }) 155 | }; 156 | } 157 | -------------------------------------------------------------------------------- /src/actions/index.js: -------------------------------------------------------------------------------- 1 | import { CALL_API, Schemas } from '../middleware/api' 2 | 3 | export const USER_REQUEST = 'USER_REQUEST' 4 | export const USER_SUCCESS = 'USER_SUCCESS' 5 | export const USER_FAILURE = 'USER_FAILURE' 6 | 7 | // Fetches a single user from Github API. 8 | // Relies on the custom API middleware defined in ../middleware/api.js. 9 | const fetchUser = login => ({ 10 | [CALL_API]: { 11 | types: [ USER_REQUEST, USER_SUCCESS, USER_FAILURE ], 12 | endpoint: `users/${login}`, 13 | schema: Schemas.USER 14 | } 15 | }) 16 | 17 | // Fetches a single user from Github API unless it is cached. 18 | // Relies on Redux Thunk middleware. 19 | export const loadUser = (login, requiredFields = []) => (dispatch, getState) => { 20 | const user = getState().entities.users[login] 21 | if (user && requiredFields.every(key => user.hasOwnProperty(key))) { 22 | return null 23 | } 24 | 25 | return dispatch(fetchUser(login)) 26 | } 27 | 28 | export const REPO_REQUEST = 'REPO_REQUEST' 29 | export const REPO_SUCCESS = 'REPO_SUCCESS' 30 | export const REPO_FAILURE = 'REPO_FAILURE' 31 | 32 | // Fetches a single repository from Github API. 33 | // Relies on the custom API middleware defined in ../middleware/api.js. 34 | const fetchRepo = fullName => ({ 35 | [CALL_API]: { 36 | types: [ REPO_REQUEST, REPO_SUCCESS, REPO_FAILURE ], 37 | endpoint: `repos/${fullName}`, 38 | schema: Schemas.REPO 39 | } 40 | }) 41 | 42 | // Fetches a single repository from Github API unless it is cached. 43 | // Relies on Redux Thunk middleware. 44 | export const loadRepo = (fullName, requiredFields = []) => (dispatch, getState) => { 45 | const repo = getState().entities.repos[fullName] 46 | if (repo && requiredFields.every(key => repo.hasOwnProperty(key))) { 47 | return null 48 | } 49 | 50 | return dispatch(fetchRepo(fullName)) 51 | } 52 | 53 | export const STARRED_REQUEST = 'STARRED_REQUEST' 54 | export const STARRED_SUCCESS = 'STARRED_SUCCESS' 55 | export const STARRED_FAILURE = 'STARRED_FAILURE' 56 | 57 | // Fetches a page of starred repos by a particular user. 58 | // Relies on the custom API middleware defined in ../middleware/api.js. 59 | const fetchStarred = (login, nextPageUrl) => ({ 60 | login, 61 | [CALL_API]: { 62 | types: [ STARRED_REQUEST, STARRED_SUCCESS, STARRED_FAILURE ], 63 | endpoint: nextPageUrl, 64 | schema: Schemas.REPO_ARRAY 65 | } 66 | }) 67 | 68 | // Fetches a page of starred repos by a particular user. 69 | // Bails out if page is cached and user didn't specifically request next page. 70 | // Relies on Redux Thunk middleware. 71 | export const loadStarred = (login, nextPage) => (dispatch, getState) => { 72 | const { 73 | nextPageUrl = `users/${login}/starred`, 74 | pageCount = 0 75 | } = getState().pagination.starredByUser[login] || {} 76 | 77 | if (pageCount > 0 && !nextPage) { 78 | return null 79 | } 80 | 81 | return dispatch(fetchStarred(login, nextPageUrl)) 82 | } 83 | 84 | export const STARGAZERS_REQUEST = 'STARGAZERS_REQUEST' 85 | export const STARGAZERS_SUCCESS = 'STARGAZERS_SUCCESS' 86 | export const STARGAZERS_FAILURE = 'STARGAZERS_FAILURE' 87 | 88 | // Fetches a page of stargazers for a particular repo. 89 | // Relies on the custom API middleware defined in ../middleware/api.js. 90 | const fetchStargazers = (fullName, nextPageUrl) => ({ 91 | fullName, 92 | [CALL_API]: { 93 | types: [ STARGAZERS_REQUEST, STARGAZERS_SUCCESS, STARGAZERS_FAILURE ], 94 | endpoint: nextPageUrl, 95 | schema: Schemas.USER_ARRAY 96 | } 97 | }) 98 | 99 | // Fetches a page of stargazers for a particular repo. 100 | // Bails out if page is cached and user didn't specifically request next page. 101 | // Relies on Redux Thunk middleware. 102 | export const loadStargazers = (fullName, nextPage) => (dispatch, getState) => { 103 | const { 104 | nextPageUrl = `repos/${fullName}/stargazers`, 105 | pageCount = 0 106 | } = getState().pagination.stargazersByRepo[fullName] || {} 107 | 108 | if (pageCount > 0 && !nextPage) { 109 | return null 110 | } 111 | 112 | return dispatch(fetchStargazers(fullName, nextPageUrl)) 113 | } 114 | 115 | export const RESET_ERROR_MESSAGE = 'RESET_ERROR_MESSAGE' 116 | 117 | // Resets the currently visible error message. 118 | export const resetErrorMessage = () => ({ 119 | type: RESET_ERROR_MESSAGE 120 | }) 121 | -------------------------------------------------------------------------------- /src/actions/positionActions.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import * as types from '../constants/ActionTypes'; 3 | import endpoints, { positionUrl } from '../lib/utils'; 4 | 5 | 6 | // Get Popular Stocks GET https://brokerage-static.s3.amazonaws.com/popular_stocks/data.json 7 | 8 | // Get Top Movers on the S&P500 GET /midlands/movers/sp500/?direction=['up' or 'down'] 9 | 10 | /* //////////////////////////////// 11 | // Positions Positions // 12 | /////////////////////////////////*/ 13 | 14 | export function requestPosition(symbol) { 15 | return: { 16 | type: types.REQUEST_POSITION, 17 | symbol 18 | }; 19 | } 20 | 21 | export function recievePosition(instrument, json) { 22 | return { 23 | type: types.RECIEVE_POSITION, 24 | data: json.data.children.map(child => child.data), 25 | instrument, 26 | recievedAt: Date.now(), 27 | } 28 | 29 | export function requestPositionError() { 30 | return { 31 | type: types.REQUEST_POSITION_ERROR, 32 | error: true, 33 | recievedAt: Date.now() 34 | }; 35 | } 36 | 37 | export function requestPositions(symbol) { 38 | return: { 39 | type: types.REQUEST_POSITION, 40 | symbol 41 | }; 42 | } 43 | 44 | export function recievePositions(json) { 45 | return { 46 | type: types.RECIEVE_POSITION, 47 | data: json.data.children.map(child => child.data), 48 | instrument, 49 | recievedAt: Date.now(), 50 | } 51 | 52 | function fetchPosition(instrument) { 53 | return dispatch => { 54 | dispatch(requestPosition(instrument)); // Show loading spinner 55 | 56 | return fetch(positionUrl(instrument)) 57 | .then(response => response.json()) 58 | .then(json => { 59 | dispatch(recievePosition(symbol, json)); 60 | }) 61 | .then(checkStatus) 62 | .catch(error => { 63 | dispatch(requestPositionError(true)) 64 | }) 65 | }; 66 | } 67 | 68 | function fetchPositions() { 69 | return dispatch => { 70 | //dispatch(requestPosition(instrument)); // Show loading spinner 71 | 72 | return fetch(endpoints['positions']) 73 | .then(response => response.json()) 74 | .then(json => { 75 | dispatch(recievePositions(json)); 76 | }) 77 | .then(checkStatus) 78 | }; 79 | } 80 | 81 | function shouldFetchPosition(instrument) { 82 | const positions = state.account.positions; 83 | 84 | if (_.isEmpty(positions)) { 85 | return true 86 | } 87 | if (positions.isFetching) { 88 | return false 89 | } 90 | return dispatch(fetchPosition(instrument)); 91 | } 92 | 93 | export function fetchPositionsIfNeeded(instrument) { 94 | return (dispatch, getState) => { 95 | if (shouldFetchPosition(getState(), instrument)) { 96 | return dispatch(fetchPosition(instrument)) 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/components/AboutPage.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const AboutPage = () => { 4 | return { 5 |
6 |

About Page

7 | 8 |

An implementation of ReactJS and Redux with the Robinhood API to make trades on your desktop machine!

9 |
10 | }; 11 | }; 12 | 13 | export default AboutPage 14 | -------------------------------------------------------------------------------- /src/components/Account.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | 3 | 4 | class Account extends Component { 5 | 6 | componentWillMount() { 7 | if(!this.props.account.lastUpdated) { 8 | this.props.fetchAccountIfNeeded(); 9 | } 10 | } 11 | 12 | render() { 13 | const { authed, account } = this.props; 14 | 15 | return ( 16 | //TODO 17 | ); 18 | } 19 | } 20 | 21 | Account.propTypes = { 22 | authed: PropTypes.object.isRequired, 23 | account: PropTypes.object.isRequired, 24 | } 25 | 26 | export default Account; 27 | -------------------------------------------------------------------------------- /src/components/AccountPane.js: -------------------------------------------------------------------------------- 1 | /* Small pane upper left corner, shows portfolio value and TODO */ 2 | -------------------------------------------------------------------------------- /src/components/App.js: -------------------------------------------------------------------------------- 1 | const App = () => ( 2 |
3 | //TODO 4 |
5 | ) 6 | -------------------------------------------------------------------------------- /src/components/History.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moosh3/robinhood-react/3c89e4016a44102d62962bbeceb43dcf333c6974/src/components/History.js -------------------------------------------------------------------------------- /src/components/LandingPage.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const AboutPage = () => { 4 | return { 5 |
6 |

About Page

7 | 8 |

An implementation of ReactJS and Redux with the Robinhood API to make trades on your desktop machine!

9 |
10 | }; 11 | }; 12 | 13 | export default LandingPage; 14 | -------------------------------------------------------------------------------- /src/components/Login.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import loginUser from '../actions/auth'; 3 | 4 | class LoginPage extends Component { 5 | 6 | constructor(props) { 7 | super(props) 8 | 9 | this.onSubmit = this.onSubmit.bind(this); 10 | } 11 | 12 | onSubmit() { 13 | dispatch(loginUser(username, password)); 14 | } 15 | 16 | render() { 17 | const { username, password } = state; 18 | 19 | return( 20 |
21 | this.username} 23 | /> 24 | this.password} 27 | /> 28 |
29 | ); 30 | } 31 | 32 | submit() { 33 | const username = this.username; 34 | const password = this.password; 35 | } 36 | } 37 | 38 | Account.PropTypes = { 39 | username: PropTypes.string.isRequired, 40 | password: PropTypes.string.isRequired 41 | } 42 | 43 | export default Account; 44 | -------------------------------------------------------------------------------- /src/components/News.js: -------------------------------------------------------------------------------- 1 | /* Includes Movers actions */ 2 | 3 | export default News 4 | -------------------------------------------------------------------------------- /src/components/NotFoundPage.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const AboutPage = () => { 4 | return { 5 |
6 |

About Page

7 | 8 |

An implementation of ReactJS and Redux with the Robinhood API to make trades on your desktop machine!

9 |
10 | }; 11 | }; 12 | 13 | export default AboutPage 14 | -------------------------------------------------------------------------------- /src/components/Order.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | 3 | class Order extends Component { 4 | /* Quote component that is rendered when a user 5 | selects a singular quote to view fundamentals, basic 6 | info, etc. Should already have quoteData['symbol'], so 7 | we can use that to dispatch the data 8 | */ 9 | 10 | /* 11 | componentWillMount() { 12 | const { dispatch, symbol } = this.props; 13 | dispatch(fetchQuote(symbol)); 14 | } 15 | 16 | 17 | shouldComponentUpdate(nextProps) { 18 | const { dispatch, symbol, updatedAt } = this.props; 19 | if (nextProps.updatedAt < ) { 20 | dispatch(fetchSongIfNeeded(nextProps.songId)); 21 | } 22 | } 23 | */ 24 | 25 | render() { 26 | const { authed, dispatch, quoteData, symbol } = this.props; 27 | 28 | return ( 29 |
30 |

Order

31 |

Shares: 12

32 |
33 | ) 34 | } 35 | } 36 | 37 | Quote.propTypes = { 38 | authed: PropTypes.bool, 39 | dispatch: PropTypes.func.isRequired, 40 | quoteData: PropTypes.func.isRequired, 41 | symbol: PropTypes.string.isRequired, 42 | }; 43 | 44 | export default Order; 45 | -------------------------------------------------------------------------------- /src/components/PositionList.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import _ from 'lodash'; 3 | import { formatCurrency, formatPercent } from '../lib/formaters'; 4 | import { fetchPosition, fetchPositionsIfNeeded } from '../actions/positionActions'; 5 | 6 | class Position extends Component { 7 | 8 | componentWillMount() { 9 | const { dispatch, symbol } = this.props; 10 | dispatch(fetchPosition(symbol)); 11 | } 12 | 13 | render() { 14 | const { authed, position } = this.props; 15 | 16 | return ( 17 | //TODO 18 | ); 19 | } 20 | 21 | } 22 | 23 | class PositionList extends Component { 24 | 25 | componentWillMount() { 26 | const { authed, account, positions } = this.props; 27 | dispatch(fetchPositionsIfNeeded(account)); 28 | } 29 | 30 | render() { 31 | const { positions, authed } = this.props; 32 | 33 | return ( 34 | {(_.values(positions.items) || []).map((stock, i) => { 35 | const { quantity, quote, instrument } = stock; 36 | let displayedValue = formatCurrency(+quote.last_trade_price); 37 | 38 | switch(settings.displayedValue) { 39 | case 'price': 40 | displayedValue = formatCurrency(+quote.last_trade_price); 41 | break; 42 | case 'equity': 43 | displayedValue = formatCurrency(+quote.last_trade_price * +quantity); 44 | break; 45 | case 'percent': 46 | displayedValue = formatPercent((+quote.last_trade_price / +quote.adjusted_previous_close) - 1); 47 | break; 48 | } 49 | ); 50 | } 51 | } 52 | 53 | Positions.propTypes = { 54 | authed: PropTypes.object.isRequired, 55 | account: PropTypes.object.isRequired, 56 | positions: PropTypes.object.isRequired, 57 | } 58 | 59 | export default PositionList; 60 | -------------------------------------------------------------------------------- /src/components/QuotePane.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import fetchQuote from '../actions/QuoteActions'; 3 | 4 | class QuotePane extends Component { 5 | /* Quote component that is rendered when a user 6 | selects a singular quote to view fundamentals, basic 7 | info, etc. Should already have quoteData['symbol'], so 8 | we can use that to dispatch the data 9 | */ 10 | 11 | componentWillMount() { 12 | const { dispatch, symbol } = this.props; 13 | dispatch(fetchQuote(symbol)); 14 | } 15 | 16 | /* 17 | shouldComponentUpdate(nextProps) { 18 | const { dispatch, symbol, updatedAt } = this.props; 19 | if (nextProps.updatedAt < ) { 20 | dispatch(fetchSongIfNeeded(nextProps.songId)); 21 | } 22 | } 23 | */ 24 | 25 | render() { 26 | const { authed, dispatch, quoteData, symbol } = this.props; 27 | 28 | return ( 29 |
30 |

Quote

31 |

Ticker: {symbol}

32 |
33 | ) 34 | } 35 | } 36 | 37 | Quote.propTypes = { 38 | authed: PropTypes.bool, 39 | dispatch: PropTypes.func.isRequired, 40 | symbol: PropTypes.string.isRequired 41 | }; 42 | 43 | export default QuotePaneComponent; 44 | -------------------------------------------------------------------------------- /src/components/Root.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import { Provider } from 'react-redux'; 3 | import { Router, Route, browserHistory } from 'react-router'; 4 | import App from './App'; 5 | 6 | const Root = ({ store }) => ( 7 | 8 | 9 | 10 | 11 | 12 | ); 13 | 14 | Root.propTypes = { 15 | store: PropTypes.object.isRequired, 16 | }; 17 | 18 | export default Root; 19 | -------------------------------------------------------------------------------- /src/components/Search.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, Component } from 'react'; 2 | import _ from 'lodash'; 3 | import SearchResults from '../components/SearchResults'; 4 | 5 | class Search extends Component { 6 | 7 | handleSubmit() { 8 | 9 | } 10 | 11 | handleChange() { 12 | 13 | } 14 | 15 | render () { 16 | const { authed, handleSubmit, handleChange } = this.props; 17 | return ( 18 |
19 |
20 | 21 |
22 |
23 | ); 24 | } 25 | } 26 | 27 | Search.propTypes = { 28 | authed: PropTypes.object.isRequired, 29 | handleChange: PropTypes.func.isRequired, 30 | handleSubmit: PropTypes.func.isRequired, 31 | }; 32 | 33 | export default Search; 34 | -------------------------------------------------------------------------------- /src/components/SearchResults.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, Component, findDOMNode } from 'react'; 2 | import _ from 'lodash'; 3 | import shallowEqual from 'react-pure-render/shallowEqual'; 4 | 5 | import SearchResultItem from './SearchResultItem'; 6 | //import Spinner from './Spinner'; 7 | 8 | import '../../assets/stylesheets/search-results.scss'; 9 | 10 | export default class SearchResults extends Component { 11 | 12 | constructor(props) { 13 | super(props); 14 | this.handleResize = this.handleResize.bind(this); 15 | this.handleScroll = this.handleScroll.bind(this); 16 | 17 | this.state = { 18 | windowWidth: window.innerWidth, 19 | windowHeight: window.innerHeight, 20 | bounds: {}, 21 | }; 22 | 23 | this.calledTriggerPositions = {}; 24 | } 25 | 26 | componentDidMount () { 27 | window.addEventListener('resize', this.handleResize); 28 | window.addEventListener('scroll', this.handleScroll); 29 | this.handleResize(); 30 | } 31 | 32 | componentDidUpdate (prevProps, prevState) { 33 | if (!shallowEqual(this.props.searchResults, prevProps.searchResults)) this.handleResize(); 34 | } 35 | 36 | componentWillUnmount () { 37 | window.removeEventListener('resize', this.handleResize); 38 | window.removeEventListener('scroll', this.handleScroll); 39 | } 40 | 41 | render () { 42 | const { searchResults, onPackageOpen } = this.props; 43 | const packages = searchResults.items || []; 44 | const loading = _.get(searchResults, 'isFetching') || false; 45 | 46 | const maxRank = _.max(_.map(packages, (item) => item.rank)); 47 | 48 | return ( 49 |
50 |
51 | {_.map(packages, (item, index) => 52 | 58 | )} 59 | 60 |
61 | 62 |
63 | ); 64 | } 65 | 66 | handleResize (e) { 67 | const el = findDOMNode(this); 68 | const bounds = (el) ? el.getBoundingClientRect() : {}; 69 | 70 | this.setState({ 71 | windowWidth: window.innerWidth, 72 | windowHeight: window.innerHeight, 73 | bounds: _.pick(bounds, ['bottom', 'height', 'left', 'right', 'top', 'width']), 74 | }); 75 | } 76 | 77 | handleScroll (e) { 78 | const threshold = 100; 79 | const { windowHeight, bounds } = this.state; 80 | const { search, onSearch } = this.props; 81 | 82 | const scrollTop = (window.pageYOffset !== undefined) ? window.pageYOffset : (document.documentElement || document.body.parentNode || document.body).scrollTop; 83 | 84 | const triggerPosition = (bounds.height - threshold); 85 | 86 | if ((scrollTop + windowHeight) > triggerPosition && !this.calledTriggerPositions[triggerPosition]) { 87 | this.calledTriggerPositions[triggerPosition] = true; 88 | if (onSearch) onSearch(search.term, search.next); 89 | } 90 | } 91 | } 92 | 93 | SearchResults.propTypes = { 94 | searchResults: PropTypes.object, 95 | onPackageOpen: PropTypes.func, 96 | }; 97 | -------------------------------------------------------------------------------- /src/components/Sidebar.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import { loginUser, logoutUser } from '../actions/authActions'; 3 | 4 | class Header extends Component { 5 | 6 | constructor(props) { 7 | super(props); 8 | this.login = this.login.bind(this); 9 | this.logout = this.logout.bind(this); 10 | } 11 | 12 | login(e) { 13 | e.preventDefault(); 14 | const { dispatch } = this.props; 15 | dispatch(loginUser()); 16 | } 17 | 18 | logout(e) { 19 | e.preventDefault(); 20 | const { dispatch } = this.props; 21 | dispatch(logoutUser()); 22 | } 23 | 24 | 25 | render() { 26 | const { authed } = this.props; 27 | 28 | return ( 29 | 60 |
61 | ); 62 | } 63 | } 64 | 65 | Nav.propTypes = { 66 | authed: propTypes.object.isRequired, 67 | }; 68 | 69 | export default Nav; 70 | -------------------------------------------------------------------------------- /src/components/Spinner.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | 3 | class Spinner extends Component { 4 | render() { 5 | const { authed, isLoading } = this.props; 6 | 7 | /* 8 | if (isLoading && !data.something) { 9 | return

Loading...

10 | } 11 | */ 12 | 13 | return ( 14 |

Spinner

15 | ); 16 | } 17 | } 18 | 19 | Spinner.propTypes = { 20 | authed: PropTypes.objects.isRequired, 21 | isLoading: PropTypes.bool.isRequired, 22 | }; 23 | 24 | export default Spinner; 25 | -------------------------------------------------------------------------------- /src/components/Watchlist.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import _ from 'lodash'; 3 | import { fetchWatchlistIfNeeded } from '../actions/watchlistActions'; 4 | 5 | class Watchlist extends Component { 6 | componentWillMount() { 7 | if (!this.props.watchlist.lastUpdated) { 8 | this.props.fetchWatchlistIfNeeded(); 9 | } 10 | } 11 | 12 | render() { 13 | const { authed, watchlist } = this.props; 14 | 15 | if (authed.true) { 16 | return ( 17 | //TODO 18 | ) 19 | } 20 | } 21 | } 22 | 23 | Watchlist.propTypes = { 24 | authed: PropTypes.object.isRequired, 25 | watchlist: PropTypes.object.isRequired, 26 | } 27 | 28 | export default Watchlist; 29 | -------------------------------------------------------------------------------- /src/components/common/Button.js: -------------------------------------------------------------------------------- 1 | const Button = props =>