├── src ├── styles │ ├── HistoryPage.css │ ├── Tabs.css │ ├── ListContainer.css │ ├── arrow.png │ ├── logo.png │ ├── Quotes.css │ ├── eprint_1024x1024.png │ ├── onTheMoon1920x1200.jpg │ ├── Margin.css │ ├── HistoryPriceDisplay.css │ ├── SectionWrapper.css │ ├── AddButton.css │ ├── Earnings.css │ ├── BuyingPower.css │ ├── LeftPanelItem.css │ ├── Statistics.css │ ├── RemoveButton.css │ ├── PortfolioPage.css │ ├── Input.css │ ├── Position.css │ ├── News.css │ ├── PortfolioValue.css │ ├── Dashboard.css │ ├── List.css │ ├── Orders.css │ ├── Search.css │ ├── PriceAlertPage.css │ ├── LeftPanelModule.css │ ├── LeftPanelFolder.css │ ├── DashboardPage.css │ ├── LoginPage.css │ ├── PriceAlertToggle.css │ ├── Instrument.css │ ├── OrderDetail.css │ ├── App.css │ └── PlaceOrder.css ├── components │ ├── AddButton.js │ ├── RemoveButton.js │ ├── Posts.js │ ├── StatisticsCard.js │ ├── SectionWrapper.js │ ├── Picker.js │ ├── NewsCard.js │ ├── WithTimeTooltip.js │ ├── LeftPanelItem.js │ ├── Input.js │ ├── WithoutTimeTooltip.js │ ├── DummyQuotes.js │ ├── PieChartTooltip.js │ ├── Margin.js │ ├── LeftPanelModule.js │ ├── PriceAlertTicker.js │ ├── LeftPanelFolder.js │ ├── Dashboard.js │ ├── Position.js │ ├── Symbol.js │ ├── QuotesForPortfolios.js │ ├── Quotes.js │ ├── EarningsChart.js │ ├── Search.js │ ├── ListContainer.js │ ├── HistoryPriceDisplay.js │ ├── Orders.js │ └── List.js ├── actions │ ├── action_ui.js │ ├── index.js │ ├── action_tabs.js │ ├── action_cards.js │ ├── action_news.js │ ├── action_earnings.js │ ├── action_markets.js │ ├── action_fundamentals.js │ ├── action_instruments.js │ ├── action_monitor.js │ ├── action_login.js │ ├── action_quotes.js │ ├── action_watchlists.js │ ├── action_portfolios.js │ └── action_positions.js ├── reducers │ ├── reducer_cards.js │ ├── reducer_news.js │ ├── reducer_ui.js │ ├── reducer_earnings.js │ ├── reducer_fundamentals.js │ ├── reducer_markets.js │ ├── reducer_instruments.js │ ├── reducer_account.js │ ├── reducer_token.js │ ├── index.js │ ├── reducer_watchlists.js │ ├── reducer_portfolios.js │ ├── reducer_monitor.js │ ├── reducer_tabs.js │ ├── reducer_quotes.js │ ├── reducer_positions.js │ ├── reducer_orders.js │ └── reducer_local.js ├── electron-wait-react.js ├── containers │ ├── App.js │ ├── PriceAlertToggle.js │ ├── PriceAlertPage.js │ ├── EditableLocalPositions.js │ ├── News.js │ ├── EditableLocalWatchLists.js │ ├── Statistics.js │ ├── PendingOrdersPage.js │ ├── Earnings.js │ ├── HistoryPage.js │ ├── LoginPage.js │ ├── BuyingPower.js │ ├── RightPanel.js │ └── PortfolioValue.js ├── index.js └── utils.js ├── Procfile ├── icon.ico ├── public ├── favicon.ico └── index.html ├── .gitignore ├── README.md └── package.json /src/styles/HistoryPage.css: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/styles/Tabs.css: -------------------------------------------------------------------------------- 1 | .rdTabAddButton { 2 | display: none; 3 | } 4 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | react: npm start 2 | electron: node src/electron-wait-react 3 | -------------------------------------------------------------------------------- /icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardlai3582/RobInDaHood/HEAD/icon.ico -------------------------------------------------------------------------------- /src/styles/ListContainer.css: -------------------------------------------------------------------------------- 1 | .draggableListsWrapper { 2 | /*display: flex;*/ 3 | } 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardlai3582/RobInDaHood/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /src/styles/arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardlai3582/RobInDaHood/HEAD/src/styles/arrow.png -------------------------------------------------------------------------------- /src/styles/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardlai3582/RobInDaHood/HEAD/src/styles/logo.png -------------------------------------------------------------------------------- /src/styles/Quotes.css: -------------------------------------------------------------------------------- 1 | .quotesWrapper { 2 | width: 100%; 3 | background-color: black; 4 | max-height: 500px; 5 | } 6 | -------------------------------------------------------------------------------- /src/styles/eprint_1024x1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardlai3582/RobInDaHood/HEAD/src/styles/eprint_1024x1024.png -------------------------------------------------------------------------------- /src/styles/onTheMoon1920x1200.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edwardlai3582/RobInDaHood/HEAD/src/styles/onTheMoon1920x1200.jpg -------------------------------------------------------------------------------- /src/styles/Margin.css: -------------------------------------------------------------------------------- 1 | .marginEach { 2 | display: flex; 3 | padding: 10px 0; 4 | } 5 | 6 | .marginEachTitle { 7 | width: 50%; 8 | color: black; 9 | } 10 | 11 | .marginEachValue { 12 | width: 50%; 13 | } 14 | -------------------------------------------------------------------------------- /src/components/AddButton.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import '../styles/AddButton.css' 3 | 4 | const AddButton = ({cb}) => ( 5 |
6 | + 7 |
8 | ) 9 | 10 | export default AddButton 11 | -------------------------------------------------------------------------------- /src/styles/HistoryPriceDisplay.css: -------------------------------------------------------------------------------- 1 | .priceRelatedWrapper { 2 | background-color: black; 3 | text-align: center; 4 | padding: 10px 0; 5 | } 6 | 7 | .priceRelatedWrapper > .last_trade_price { 8 | font-size: 1.4rem; 9 | margin-bottom: 3px; 10 | } 11 | -------------------------------------------------------------------------------- /src/styles/SectionWrapper.css: -------------------------------------------------------------------------------- 1 | .SectionWrapper { 2 | padding: 20px; 3 | } 4 | 5 | .sectionH3 { 6 | color: #555869; 7 | padding: 0px 0px 5px 10px; 8 | } 9 | 10 | .sectionChildrenWrapper { 11 | /*background-color: teal;*/ 12 | padding: 10px; 13 | } 14 | -------------------------------------------------------------------------------- /src/styles/AddButton.css: -------------------------------------------------------------------------------- 1 | .addButtonBorder { 2 | position: relative; 3 | width: 30px; 4 | height: 30px; 5 | border: 5px solid white; 6 | border-radius: 50%; 7 | 8 | font-size: 2rem; 9 | text-align: center; 10 | line-height: 30px; 11 | 12 | cursor: pointer; 13 | } 14 | -------------------------------------------------------------------------------- /src/components/RemoveButton.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import '../styles/RemoveButton.css' 3 | 4 | const RemoveButton = ({cb}) => ( 5 |
6 |
7 |
8 | ) 9 | 10 | export default RemoveButton 11 | -------------------------------------------------------------------------------- /src/styles/Earnings.css: -------------------------------------------------------------------------------- 1 | .earningsWrapper { 2 | 3 | } 4 | 5 | .epsTextWrapper { 6 | display: flex; 7 | margin: 0 auto; 8 | width: 90%; 9 | } 10 | 11 | .epsTextWrapper > div { 12 | width: 50%; 13 | margin: 10px 0; 14 | } 15 | 16 | .epsTextWrapper > div > h6 { 17 | margin: 10px 0 5px; 18 | } 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | /dist 12 | 13 | # misc 14 | .DS_Store 15 | .env 16 | npm-debug.log* 17 | yarn-debug.log* 18 | yarn-error.log* 19 | 20 | -------------------------------------------------------------------------------- /src/components/Posts.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react' 2 | 3 | const Posts = ({posts}) => ( 4 | 9 | ) 10 | 11 | Posts.propTypes = { 12 | posts: PropTypes.array.isRequired 13 | } 14 | 15 | export default Posts 16 | -------------------------------------------------------------------------------- /src/styles/BuyingPower.css: -------------------------------------------------------------------------------- 1 | .buyingPowerWrapper { 2 | 3 | } 4 | 5 | .each { 6 | display: flex; 7 | } 8 | 9 | .each:last-child { 10 | border-top: 1px solid var(--main-color); 11 | font-weight: bold; 12 | } 13 | 14 | .each > div { 15 | width: 33%; 16 | text-align: right; 17 | padding: 5px; 18 | } 19 | 20 | .each > div:first-child { 21 | width: 33%; 22 | text-align: left; 23 | } 24 | -------------------------------------------------------------------------------- /src/styles/LeftPanelItem.css: -------------------------------------------------------------------------------- 1 | .leftPanelItemWrapper { 2 | display: flex; 3 | justify-content:flex-start; 4 | align-items: baseline; 5 | } 6 | 7 | .symbolDiv { 8 | min-width: 25%; 9 | } 10 | 11 | .infoDiv { 12 | font-size: 0.8rem; 13 | width: 70%; 14 | margin-left: 8%; 15 | display: flex; 16 | justify-content:flex-start; 17 | text-align: right; 18 | padding-right: 5px; 19 | } 20 | -------------------------------------------------------------------------------- /src/styles/Statistics.css: -------------------------------------------------------------------------------- 1 | .statisticsWrapper{ 2 | width: 100%; 3 | display: flex; 4 | flex-wrap:wrap; 5 | /*justify-content:space-around;*/ 6 | } 7 | 8 | .statisticsDiv { 9 | width: 50%; 10 | box-sizing: border-box; 11 | margin-bottom: 12px; 12 | /*padding: 10px;*/ 13 | } 14 | 15 | .statisticsNum { 16 | 17 | } 18 | 19 | .statisticsType { 20 | color: black; 21 | padding-top: 2px; 22 | padding-left: 1px; 23 | } 24 | -------------------------------------------------------------------------------- /src/styles/RemoveButton.css: -------------------------------------------------------------------------------- 1 | .removeButtonBorder { 2 | position: relative; 3 | width: 30px; 4 | height: 30px; 5 | border: 5px solid white; 6 | border-radius: 50%; 7 | 8 | font-size: 2rem; 9 | text-align: center; 10 | line-height: 30px; 11 | 12 | cursor: pointer; 13 | } 14 | 15 | .removeSign { 16 | width: 15px; 17 | border-top: 4px solid white; 18 | border-radius: 0px; 19 | position: relative; 20 | top: 13px; 21 | left: 7px; 22 | } 23 | -------------------------------------------------------------------------------- /src/components/StatisticsCard.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react' 2 | import '../styles/Statistics.css' 3 | 4 | const StatisticsCard = ({ type, num }) => ( 5 |
6 |
{num}
7 |
{type}
8 |
9 | ) 10 | 11 | StatisticsCard.propTypes = { 12 | type: PropTypes.string.isRequired, 13 | num: PropTypes.string.isRequired 14 | } 15 | 16 | export default StatisticsCard 17 | -------------------------------------------------------------------------------- /src/actions/action_ui.js: -------------------------------------------------------------------------------- 1 | ////////////UI 2 | export const TOGGLE_WATCHLISTS_MODULE = 'TOGGLE_WATCHLISTS_MODULE' 3 | export const TOGGLE_POSITIONS_MODULE = 'TOGGLE_POSITIONS_MODULE' 4 | export const RESET_BOTH_MODULE = 'RESET_BOTH_MODULE' 5 | 6 | export const resetModule = () => ({ 7 | type: RESET_BOTH_MODULE 8 | }) 9 | 10 | export const toggleWatchlistsModule = () => ({ 11 | type: TOGGLE_WATCHLISTS_MODULE 12 | }) 13 | 14 | export const togglePositionsModule = () => ({ 15 | type: TOGGLE_POSITIONS_MODULE 16 | }) 17 | -------------------------------------------------------------------------------- /src/styles/PortfolioPage.css: -------------------------------------------------------------------------------- 1 | .addFolderWrapper { 2 | display: flex; 3 | justify-content:space-between; 4 | align-items:flex-end; 5 | margin-bottom: 5px; 6 | border-bottom: 1px solid #46E3C1; 7 | } 8 | 9 | .addFolderWrapper > h6 { 10 | font-size: 2rem; 11 | } 12 | 13 | .addFolderButton { 14 | background-color: white; 15 | border: none; 16 | padding: 5px 10px; 17 | cursor: pointer; 18 | color: #2B756A; 19 | margin-bottom: 5px; 20 | } 21 | 22 | .addFolderButton:hover { 23 | background-color: #E0F7F1; 24 | } 25 | -------------------------------------------------------------------------------- /src/reducers/reducer_cards.js: -------------------------------------------------------------------------------- 1 | import * as actions from '../actions'; 2 | 3 | const cardsReducer = (state = { 4 | cards: [], 5 | cardsLastUpdated: "" 6 | }, action) => { 7 | switch (action.type) { 8 | case actions.CARDS_DELETE: 9 | return { 10 | ...state, 11 | cards: [] 12 | } 13 | case actions.CARDS_ADD: 14 | return { 15 | ...state, 16 | cards: action.cards, 17 | cardsLastUpdated: `${Date.now()}` 18 | } 19 | default: 20 | return state 21 | } 22 | } 23 | 24 | export default cardsReducer 25 | -------------------------------------------------------------------------------- /src/styles/Input.css: -------------------------------------------------------------------------------- 1 | .loginInputWrapper { 2 | margin: 10px; 3 | font-size: 1.2rem; 4 | } 5 | 6 | .loginInputWrapper label{ 7 | text-align: left; 8 | } 9 | 10 | .loginInput { 11 | background-color: black; 12 | color: var(--main-color);/*#EBB03F;*/ 13 | border-width: 1px; 14 | border-style: solid; 15 | border-color: var(--main-color);/*#EBB03F;*/ 16 | border-radius: 5px; 17 | padding: 3px 5px; 18 | margin: 10px 0; 19 | width: 200px; 20 | } 21 | 22 | .message { 23 | color: red; 24 | font-size: 1rem; 25 | margin-left: 5px; 26 | height: 1rem; 27 | } 28 | -------------------------------------------------------------------------------- /src/styles/Position.css: -------------------------------------------------------------------------------- 1 | .positionWRapper { 2 | width: 100%; 3 | } 4 | 5 | .upperPosition { 6 | display: flex; 7 | } 8 | 9 | .upperDiv { 10 | width: 50%; 11 | padding-top: 10px; 12 | } 13 | 14 | .lowerPosition { 15 | margin-top: 15px; 16 | border-top: 1px solid var(--main-color); 17 | padding-top: 10px; 18 | } 19 | 20 | .positionNum { 21 | 22 | } 23 | 24 | .positionType { 25 | color: black; 26 | padding-top: 2px; 27 | padding-left: 1px; 28 | } 29 | 30 | .lowerDiv { 31 | display: flex; 32 | margin-bottom: 10px; 33 | } 34 | 35 | .lowerDiv > div { 36 | width: 50%; 37 | } 38 | -------------------------------------------------------------------------------- /src/actions/index.js: -------------------------------------------------------------------------------- 1 | export * from './action_login' 2 | export * from './action_portfolios' 3 | export * from './action_watchlists' 4 | export * from './action_positions' 5 | export * from './action_instruments' 6 | export * from './action_tabs' 7 | export * from './action_fundamentals' 8 | export * from './action_news' 9 | export * from './action_quotes' 10 | export * from './action_orders' 11 | export * from './action_markets' 12 | export * from './action_ui' 13 | export * from './action_local' 14 | export * from './action_cards' 15 | export * from './action_monitor' 16 | export * from './action_earnings' 17 | -------------------------------------------------------------------------------- /src/components/SectionWrapper.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react' 2 | import '../styles/SectionWrapper.css' 3 | 4 | const SectionWrapper = ({ SectionTitle, children, backgroundColor }) => { 5 | if(!backgroundColor) backgroundColor = "teal" 6 | 7 | return ( 8 |
9 |

{SectionTitle}

10 |
{children}
11 |
12 | ) 13 | } 14 | 15 | 16 | SectionWrapper.propTypes = { 17 | SectionTitle: PropTypes.string.isRequired 18 | } 19 | 20 | export default SectionWrapper 21 | -------------------------------------------------------------------------------- /src/components/Picker.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react' 2 | 3 | const Picker = ({ value, onChange, options }) => ( 4 | 5 |

{value}

6 | 14 |
15 | ) 16 | 17 | Picker.propTypes = { 18 | options: PropTypes.arrayOf( 19 | PropTypes.string.isRequired 20 | ).isRequired, 21 | value: PropTypes.string.isRequired, 22 | onChange: PropTypes.func.isRequired 23 | } 24 | 25 | export default Picker 26 | -------------------------------------------------------------------------------- /src/components/NewsCard.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react' 2 | import '../styles/News.css' 3 | 4 | const NewsCard = ({ url, title, published_at, openUrlInBrowser }) => ( 5 |
6 |
7 | 8 | {title} 9 | 10 |
11 |
{published_at}
12 |
13 | ) 14 | 15 | NewsCard.propTypes = { 16 | url: PropTypes.string.isRequired, 17 | title: PropTypes.string.isRequired, 18 | published_at: PropTypes.string.isRequired, 19 | openUrlInBrowser: PropTypes.func.isRequired 20 | } 21 | 22 | export default NewsCard 23 | -------------------------------------------------------------------------------- /src/reducers/reducer_news.js: -------------------------------------------------------------------------------- 1 | import * as actions from '../actions'; 2 | 3 | const newsReducer = (state = { 4 | newsAll: {} 5 | }, action) => { 6 | switch (action.type) { 7 | case actions.NEWS_ADD: 8 | let newNewsAll = Object.assign({}, state.newsAll); 9 | newNewsAll[action.symbol] = action.news; 10 | return { 11 | ...state, 12 | newsAll: newNewsAll 13 | } 14 | case actions.NEWS_DELETE: 15 | let tempNewsAll = Object.assign({}, state.newsAll); 16 | delete tempNewsAll[action.symbol]; 17 | return { 18 | ...state, 19 | newsAll: tempNewsAll 20 | } 21 | default: 22 | return state 23 | } 24 | } 25 | 26 | export default newsReducer 27 | -------------------------------------------------------------------------------- /src/electron-wait-react.js: -------------------------------------------------------------------------------- 1 | const net = require('net'); 2 | const port = process.env.PORT ? (process.env.PORT - 100) : 3000; 3 | 4 | process.env.ELECTRON_START_URL = `http://localhost:${port}`; 5 | 6 | const client = new net.Socket(); 7 | 8 | let startedElectron = false; 9 | const tryConnection = () => client.connect({port: port}, () => { 10 | client.end(); 11 | if(!startedElectron) { 12 | console.log('starting electron'); 13 | startedElectron = true; 14 | const exec = require('child_process').exec; 15 | exec('npm run electron'); 16 | } 17 | } 18 | ); 19 | 20 | tryConnection(); 21 | 22 | client.on('error', (error) => { 23 | setTimeout(tryConnection, 1000); 24 | }); 25 | -------------------------------------------------------------------------------- /src/components/WithTimeTooltip.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | 3 | class WithTimeTooltip extends Component { 4 | static propTypes = { 5 | type: PropTypes.string, 6 | payload: PropTypes.array, 7 | label: PropTypes.string, 8 | } 9 | 10 | render() { 11 | const { active } = this.props; 12 | 13 | if (active) { 14 | const { payload, label } = this.props; 15 | let date = new Date(label); 16 | let dateLabel = date.toLocaleString() 17 | 18 | return ( 19 |
20 |
{dateLabel}
21 |
{payload[0].value}
22 |
23 | ); 24 | } 25 | 26 | return null; 27 | } 28 | } 29 | 30 | export default WithTimeTooltip 31 | -------------------------------------------------------------------------------- /src/components/LeftPanelItem.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react' 2 | import '../styles/LeftPanelItem.css' 3 | 4 | const LeftPanelItem = ({ symbol, id, onClick, className, children }) => ( 5 |
6 |
{symbol}
7 | {(React.Children.toArray(children).length === 0)? null :
{children}
} 8 |
9 | ) 10 | 11 | LeftPanelItem.propTypes = { 12 | symbol: PropTypes.string.isRequired, 13 | id: PropTypes.oneOfType([ 14 | PropTypes.string, 15 | PropTypes.number 16 | ]), 17 | onClick: PropTypes.func.isRequired, 18 | className: PropTypes.string.isRequired 19 | } 20 | 21 | export default LeftPanelItem 22 | -------------------------------------------------------------------------------- /src/reducers/reducer_ui.js: -------------------------------------------------------------------------------- 1 | import * as actions from '../actions'; 2 | 3 | const uiReducer = (state = { 4 | watchlistsModuleOpen: false, 5 | positionsModuleOpen: false 6 | }, action) => { 7 | switch (action.type) { 8 | case actions.RESET_BOTH_MODULE: 9 | return { 10 | watchlistsModuleOpen: false, 11 | positionsModuleOpen: false 12 | } 13 | case actions.TOGGLE_WATCHLISTS_MODULE: 14 | return { 15 | ...state, 16 | watchlistsModuleOpen: !state.watchlistsModuleOpen 17 | } 18 | case actions.TOGGLE_POSITIONS_MODULE: 19 | return { 20 | ...state, 21 | positionsModuleOpen: !state.positionsModuleOpen 22 | } 23 | default: 24 | return state 25 | } 26 | } 27 | 28 | export default uiReducer 29 | -------------------------------------------------------------------------------- /src/reducers/reducer_earnings.js: -------------------------------------------------------------------------------- 1 | import * as actions from '../actions'; 2 | 3 | const earningsReducer = (state = { 4 | earningsAll: {} 5 | }, action) => { 6 | switch (action.type) { 7 | case actions.EARNINGS_ADD: 8 | let newEarningsAll = Object.assign({}, state.earningsAll); 9 | newEarningsAll[action.symbol] = action.earnings; 10 | return { 11 | ...state, 12 | earningsAll: newEarningsAll 13 | } 14 | case actions.EARNINGS_DELETE: 15 | let tempEarningsAll = Object.assign({}, state.earningsAll); 16 | delete tempEarningsAll[action.symbol]; 17 | return { 18 | ...state, 19 | earningsAll: tempEarningsAll 20 | } 21 | default: 22 | return state 23 | } 24 | } 25 | 26 | export default earningsReducer 27 | -------------------------------------------------------------------------------- /src/actions/action_tabs.js: -------------------------------------------------------------------------------- 1 | ////////////TABS 2 | export const TAB_ADD = 'TAB_ADD' 3 | export const TAB_DELETE = 'TAB_DELETE' 4 | export const TAB_SELECT = 'TAB_SELECT' 5 | export const TAB_REORDER = 'TAB_REORDER' 6 | export const TABS_DELETE_ALL = 'TABS_DELETE_ALL' 7 | 8 | export const deleteTabs = () => ({ 9 | type: TABS_DELETE_ALL 10 | }) 11 | 12 | export const addTab = (currentKey, newTab) => ({ 13 | type: TAB_ADD, 14 | currentKey, 15 | newTab 16 | }) 17 | 18 | export const deleteTab = (deletedKey, currentKeys) => ({ 19 | type: TAB_DELETE, 20 | deletedKey, 21 | currentKeys 22 | }) 23 | 24 | export const selectTab = (currentKey) => ({ 25 | type: TAB_SELECT, 26 | currentKey 27 | }) 28 | 29 | export const reorderTab = (currentKeys) => ({ 30 | type: TAB_REORDER, 31 | currentKeys 32 | }) 33 | -------------------------------------------------------------------------------- /src/styles/News.css: -------------------------------------------------------------------------------- 1 | .eachNews{ 2 | margin-bottom: 10px; 3 | } 4 | 5 | .newsLink { 6 | padding: 5px; 7 | } 8 | 9 | .newsLink > a { 10 | color: white; 11 | cursor: pointer; 12 | text-decoration: none; 13 | } 14 | 15 | .newsLink:hover { 16 | background-color: var(--main-color); 17 | } 18 | 19 | .dateDiv { 20 | color: black; 21 | padding: 2px 0 0 5px; 22 | } 23 | 24 | .moreNewsWrapper { 25 | display: flex; 26 | justify-content:flex-end; 27 | border-top: 1px solid var(--main-color); 28 | } 29 | 30 | .moreNews { 31 | margin-top: 3px; 32 | border: none; 33 | font-family: Arial; 34 | color: #045C4F; 35 | background: #46e3c1; 36 | padding: 5px 10px; 37 | text-decoration: none; 38 | cursor: pointer; 39 | } 40 | 41 | .moreNews:hover { 42 | background-color: var(--main-color); 43 | } 44 | -------------------------------------------------------------------------------- /src/components/Input.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react' 2 | import '../styles/Input.css' 3 | 4 | const Input = ({ type, focus, value, onChange, label, message}) => ( 5 |
6 | 11 |
{message}
12 |
13 | ) 14 | //onChange={e => onChange(e.target.value)} 15 | Input.propTypes = { 16 | type: PropTypes.string.isRequired, 17 | focus: PropTypes.bool.isRequired, 18 | label: PropTypes.string.isRequired, 19 | value: PropTypes.string.isRequired, 20 | message: PropTypes.string.isRequired, 21 | onChange: PropTypes.func.isRequired 22 | } 23 | 24 | export default Input 25 | -------------------------------------------------------------------------------- /src/components/WithoutTimeTooltip.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | 3 | class WithoutTimeTooltip extends Component { 4 | static propTypes = { 5 | type: PropTypes.string, 6 | payload: PropTypes.array, 7 | label: PropTypes.string, 8 | } 9 | 10 | render() { 11 | const { active } = this.props; 12 | 13 | if (active) { 14 | const { payload, label } = this.props; 15 | let date = new Date(label); 16 | let dateLabel = (date.getMonth()+1)+"/"+date.getDate()+"/"+date.getFullYear(); 17 | 18 | return ( 19 |
20 |
{dateLabel}
21 |
{payload[0].value}
22 |
23 | ); 24 | } 25 | 26 | return null; 27 | } 28 | } 29 | 30 | export default WithoutTimeTooltip 31 | -------------------------------------------------------------------------------- /src/styles/PortfolioValue.css: -------------------------------------------------------------------------------- 1 | .PortfolioValueWrapper { 2 | display: flex; 3 | flex-wrap: wrap; 4 | justify-content:space-around; 5 | } 6 | 7 | .pieChartWrapper { 8 | 9 | } 10 | 11 | .valueWrapper { 12 | display: flex; 13 | flex-grow: 1; 14 | flex-direction:column; 15 | justify-content:center; 16 | font-size: 1.4rem; 17 | padding: 20px; 18 | } 19 | 20 | .valueWrapper > div { 21 | padding: 10px 0; 22 | } 23 | 24 | .eachValue { 25 | display: flex; 26 | flex-direction:row; 27 | } 28 | 29 | .eachValue:last-child { 30 | border-top: 1px solid var(--main-color); 31 | } 32 | 33 | .eachValue > .valueName { 34 | width: 50%; 35 | text-align: left; 36 | padding-left: 10px; 37 | } 38 | 39 | .eachValue > .valueValue { 40 | width: 50%; 41 | text-align: right; 42 | padding-right: 10px; 43 | } 44 | -------------------------------------------------------------------------------- /src/reducers/reducer_fundamentals.js: -------------------------------------------------------------------------------- 1 | import * as actions from '../actions'; 2 | 3 | const fundamentalsReducer = (state = { 4 | fundamentals: {} 5 | }, action) => { 6 | switch (action.type) { 7 | case actions.FUNDAMENTAL_ADD: 8 | let newfundamentals = Object.assign({}, state.fundamentals); 9 | newfundamentals[action.symbol] = action.fundamental; 10 | return { 11 | ...state, 12 | fundamentals: newfundamentals 13 | } 14 | case actions.FUNDAMENTAL_DELETE: 15 | let tempfundamentals = Object.assign({}, state.fundamentals); 16 | delete tempfundamentals[action.symbol]; 17 | return { 18 | ...state, 19 | fundamentals: tempfundamentals 20 | } 21 | default: 22 | return state 23 | } 24 | } 25 | 26 | export default fundamentalsReducer 27 | -------------------------------------------------------------------------------- /src/styles/Dashboard.css: -------------------------------------------------------------------------------- 1 | .dbContainer { 2 | width: 100%; 3 | height: 100%; 4 | } 5 | 6 | .dbLeftPanel { 7 | position: absolute; 8 | left: 0; 9 | top: 0; 10 | bottom: 0; 11 | width: 200px; 12 | background-color: black; 13 | color: var(--main-color); 14 | overflow-x: hidden; 15 | overflow-y: hidden;/*scroll;*/ 16 | box-sizing: border-box; 17 | /*border-right: 1px solid white;*/ 18 | } 19 | .dbLeftPanel::-webkit-scrollbar { 20 | /*width: 0 !important*/ 21 | } 22 | 23 | .dbRightPanel { 24 | width: calc(100% - 200px); 25 | position: absolute; 26 | left: 200px; 27 | top: 0; 28 | bottom: 0; 29 | background-color: none; 30 | color: white; 31 | } 32 | 33 | .dbDrag { 34 | position: absolute; 35 | left: -4px; 36 | top: 0; 37 | bottom: 0; 38 | /*width: 8px;*/ 39 | /*cursor: w-resize;*/ 40 | background-color: none; 41 | } 42 | -------------------------------------------------------------------------------- /src/styles/List.css: -------------------------------------------------------------------------------- 1 | .listWrapper { 2 | margin-bottom: 5px; 3 | color: black; 4 | width: 100px; 5 | background-color: yellow; 6 | width: 100%; 7 | } 8 | 9 | .listWrapper:last-child { 10 | margin-bottom: 0px; 11 | } 12 | 13 | .listHeaderWrapper { 14 | display: flex; 15 | justify-content:space-between; 16 | flex-direction:row; 17 | background-color: var(--main-color); 18 | cursor: move; 19 | } 20 | 21 | .listHeaderWrapper > h3 { 22 | padding: 5px; 23 | text-align: center; 24 | color: white; 25 | margin-left: 5px; 26 | } 27 | 28 | .listHeaderWrapper > button { 29 | border: none; 30 | background-color: #FA5882; 31 | color: white; 32 | margin: 5px; 33 | border-radius: 5px; 34 | cursor: pointer; 35 | } 36 | 37 | .listWrapper > section { 38 | display: flex; 39 | flex-wrap:wrap; 40 | width: 100%; 41 | padding-bottom: 1rem; 42 | } 43 | -------------------------------------------------------------------------------- /src/styles/Orders.css: -------------------------------------------------------------------------------- 1 | .orderWrapper { 2 | border-bottom: 1px solid var(--main-color); 3 | display: flex; 4 | justify-content:space-between; 5 | align-items:center; 6 | padding: 5px; 7 | cursor: pointer; 8 | } 9 | 10 | .orderWrapper:hover { 11 | background-color: var(--main-color); 12 | } 13 | 14 | .recentOrdersWrapper > .orderWrapper:last-child { 15 | border-bottom: none; 16 | } 17 | 18 | .orderHisDate { 19 | color: black; 20 | font-size: 0.8rem; 21 | margin-top: 3px; 22 | } 23 | 24 | .orderMoreButtonWrapper { 25 | display: flex; 26 | justify-content:flex-end; 27 | border-top: 1px solid var(--main-color); 28 | } 29 | 30 | .orderMoreButton { 31 | margin-top: 3px; 32 | border: none; 33 | font-family: Arial; 34 | color: #045C4F; 35 | background: #46e3c1; 36 | padding: 5px 10px; 37 | text-decoration: none; 38 | cursor: pointer; 39 | } 40 | -------------------------------------------------------------------------------- /src/styles/Search.css: -------------------------------------------------------------------------------- 1 | .renderSuggestionsContainer{ 2 | background-color: white; 3 | position: absolute; 4 | z-index: 5; 5 | } 6 | 7 | .suggestionSpan { 8 | background-color: white; 9 | cursor: pointer; 10 | } 11 | 12 | .suggestionSpan:hover { 13 | background-color: #D8D8D8; 14 | } 15 | 16 | .suggestionSymbol { 17 | padding: 3px 5px; 18 | color: teal; 19 | } 20 | 21 | .suggestionName { 22 | color: black; 23 | padding: 0px 5px 3px; 24 | font-size: 0.75rem; 25 | border-bottom: 1px solid grey; 26 | } 27 | 28 | .renderInputComponent { 29 | margin: 3px 3px 0 3px; 30 | } 31 | 32 | .renderInputComponent > input { 33 | width: 100%; 34 | height: 30px; 35 | border: 3px solid black; 36 | border-radius: 5px; 37 | box-sizing: border-box; 38 | padding-left: 3px; 39 | text-align: left; 40 | outline: none; 41 | vertical-align: text-bottom ; 42 | } 43 | -------------------------------------------------------------------------------- /src/components/DummyQuotes.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import '../styles/Quotes.css' 3 | 4 | class DummyQuotes extends Component { 5 | 6 | constructor(props) { 7 | super(props); 8 | this.state = {width: 0}; 9 | } 10 | 11 | componentDidMount() { 12 | window.addEventListener('resize', this.resetDimensions); 13 | this.resetDimensions(); 14 | } 15 | 16 | componentWillUnmount() { 17 | window.removeEventListener('resize', this.resetDimensions); 18 | } 19 | 20 | resetDimensions = () => { 21 | this.setState({width: this.qw.offsetWidth }); 22 | this.qw.style.height = this.qw.offsetWidth>500? "250px" : this.qw.offsetWidth/2+"px"; 23 | } 24 | 25 | render() { 26 | return ( 27 |
{ this.qw = div; }} > 28 | 29 |
30 | ) 31 | } 32 | } 33 | 34 | export default DummyQuotes 35 | -------------------------------------------------------------------------------- /src/actions/action_cards.js: -------------------------------------------------------------------------------- 1 | ////////////CARDS 2 | export const CARDS_ADD = 'CARDS_ADD' 3 | export const CARDS_DELETE = 'CARDS_DELETE' 4 | 5 | export const deleteCards = () => ({ 6 | type: CARDS_DELETE 7 | }) 8 | 9 | export const addCards = (cards) => ({ 10 | type: CARDS_ADD, 11 | cards 12 | }) 13 | 14 | export const askCards = () => (dispatch, getState) => { 15 | return fetch(`https://api.robinhood.com/midlands/notifications/stack/`, { 16 | method: 'GET', 17 | headers: new Headers({ 18 | 'Accept': 'application/json', 19 | 'Authorization': getState().tokenReducer.token 20 | }) 21 | }) 22 | .then(response => response.json()) 23 | .then(jsonResult => { 24 | if(jsonResult.results) { 25 | dispatch(addCards(jsonResult.results)); 26 | } 27 | else { 28 | console.log(jsonResult); 29 | } 30 | }) 31 | .catch(function(reason) { 32 | console.log(reason); 33 | }); 34 | } 35 | -------------------------------------------------------------------------------- /src/styles/PriceAlertPage.css: -------------------------------------------------------------------------------- 1 | .priceAlertUl { 2 | padding: 20px; 3 | } 4 | 5 | .priceAlertTicker { 6 | display: flex; 7 | justify-content:space-between; 8 | flex-wrap:wrap; 9 | align-items:center; 10 | margin-bottom: 5px; 11 | background-color: teal; 12 | } 13 | 14 | .priceAlertTicker > div { 15 | font-size: 1.1rem; 16 | width: 30%; 17 | margin: 5px 0; 18 | } 19 | 20 | .priceAlertSymbol { 21 | text-align: left; 22 | padding-left: 10px; 23 | } 24 | 25 | .priceAlertTickerSetting { 26 | vertical-align: baseline; 27 | text-align: center; 28 | } 29 | 30 | .priceAlertTickerSetting > * { 31 | margin: 1px; 32 | } 33 | 34 | .priceAlertButtonWrapper { 35 | text-align: right; 36 | } 37 | 38 | .priceAlertTickerButton { 39 | background-color: #FA5882; 40 | border: none; 41 | border-radius: 5px; 42 | color: white; 43 | cursor: pointer; 44 | padding: 5px 5px; 45 | margin-right: 10px; 46 | } 47 | -------------------------------------------------------------------------------- /src/reducers/reducer_markets.js: -------------------------------------------------------------------------------- 1 | import * as actions from '../actions'; 2 | 3 | const marketsReducer = (state = { 4 | markets: {}, 5 | marketsHours: {} 6 | }, action) => { 7 | switch (action.type) { 8 | case actions.CLEAR_MARKETS_AND_MARKETSHOURS: 9 | return { 10 | markets: {}, 11 | marketsHours: {} 12 | } 13 | case actions.ADD_MARKETSHOUR: 14 | console.log(action.marketshour); 15 | let newMarketsHours = Object.assign({}, state.marketsHours); 16 | newMarketsHours[action.todays_hours] = action.marketshour; 17 | return { 18 | ...state, 19 | marketsHours: newMarketsHours 20 | } 21 | case actions.ADD_MARKET: 22 | console.log(action.market); 23 | let newMarkets = Object.assign({}, state.markets); 24 | newMarkets[action.market.url] = action.market; 25 | return { 26 | ...state, 27 | markets: newMarkets 28 | } 29 | default: 30 | return state 31 | } 32 | } 33 | 34 | export default marketsReducer 35 | -------------------------------------------------------------------------------- /src/actions/action_news.js: -------------------------------------------------------------------------------- 1 | ////////////NEWS 2 | export const NEWS_ADD = 'NEWS_ADD' 3 | export const NEWS_DELETE = 'NEWS_DELETE' 4 | 5 | export const addNews = (symbol, news) => ({ 6 | type: NEWS_ADD, 7 | symbol, 8 | news 9 | }) 10 | 11 | export const deleteNews = (symbol) => ({ 12 | type: NEWS_DELETE, 13 | symbol 14 | }) 15 | 16 | export const askNews = (symbol) => (dispatch, getState) => { 17 | return fetch(`https://api.robinhood.com/midlands/news/${symbol}/`, { 18 | method: 'GET', 19 | headers: new Headers({ 20 | 'Accept': 'application/json' 21 | }) 22 | }) 23 | .then(response => response.json()) 24 | .then(jsonResult => { 25 | dispatch(addNews(symbol, jsonResult)); 26 | }) 27 | .catch(function(reason) { 28 | console.log(reason); 29 | }); 30 | } 31 | 32 | export const cleanUpNews = () => (dispatch, getState) => { 33 | Object.keys(getState().newsReducer.newsAll).forEach((symbol) => { 34 | if(getState().tabsReducer.keys.indexOf(symbol) === -1) { 35 | dispatch(deleteNews(symbol)); 36 | } 37 | }); 38 | } 39 | -------------------------------------------------------------------------------- /src/reducers/reducer_instruments.js: -------------------------------------------------------------------------------- 1 | import * as actions from '../actions'; 2 | 3 | const instrumentsReducer = (state = { 4 | instruments: {} 5 | }, action) => { 6 | switch (action.type) { 7 | case actions.ASKING_INSTRUMENT: 8 | return { 9 | ...state 10 | } 11 | case actions.ASKING_INSTRUMENT_FAILED: 12 | console.log(action.error) 13 | return { 14 | ...state 15 | } 16 | case actions.ADD_INSTRUMENT: 17 | let newInstruments = Object.assign({}, state.instruments); 18 | newInstruments[action.instrument.url] = action.instrument; 19 | return { 20 | ...state, 21 | instruments: newInstruments 22 | } 23 | case actions.DELETE_INSTRUMENT: 24 | let newDeletedInstruments = Object.assign({}, state.instruments); 25 | delete newDeletedInstruments[action.instrumentUrl]; 26 | return { 27 | ...state, 28 | instruments: newDeletedInstruments, 29 | } 30 | default: 31 | return state 32 | } 33 | } 34 | 35 | export default instrumentsReducer 36 | -------------------------------------------------------------------------------- /src/components/PieChartTooltip.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | 3 | class PieChartTooltip extends Component { 4 | static propTypes = { 5 | type: PropTypes.string, 6 | payload: PropTypes.array, 7 | label: PropTypes.string, 8 | } 9 | 10 | render() { 11 | const { active, payload } = this.props; 12 | 13 | if (active) { 14 | let { name, quantity, last_trade_price, value, total} = payload[0]; 15 | 16 | return ( 17 |
18 |
19 | {name} 20 |
21 | {(quantity)? ( 22 |
{`${quantity} X $${last_trade_price.toFixed(2)}`}
23 | ) : null} 24 | 25 |
26 | {(total)? `$${value.toFixed(2)} (${(value/total*100).toFixed(2)}%)` : `$${value.toFixed(2)}` } 27 |
28 |
29 | ); 30 | } 31 | 32 | return null; 33 | } 34 | } 35 | 36 | export default PieChartTooltip 37 | -------------------------------------------------------------------------------- /src/styles/LeftPanelModule.css: -------------------------------------------------------------------------------- 1 | .moduleWrapper { 2 | text-align: left; 3 | } 4 | 5 | .moduleArrow{ 6 | position: relative; 7 | background-color: none; 8 | z-index: 1; 9 | padding-left: 10px; 10 | } 11 | 12 | .moduleTitle { 13 | font-size: 1.2rem; 14 | cursor: pointer; 15 | padding: 5px 0 5px 0px; 16 | } 17 | 18 | .open { 19 | top: -2px; 20 | width: 0.6rem; 21 | height: 0.6rem; 22 | margin-left: 0px; 23 | padding-right: 5px; 24 | transform: rotate(0deg); 25 | } 26 | 27 | .close { 28 | top: 0px; 29 | width: 0.6rem; 30 | height: 0.6rem; 31 | padding-right: 5px; 32 | transform: rotate(-90deg); 33 | } 34 | 35 | .moduleDiv { 36 | padding-left: calc( 25px + 0.6rem); 37 | cursor: pointer; 38 | background-color: #313E3B; 39 | padding-top: 2px; 40 | padding-bottom: 2px; 41 | border-bottom: 1px solid black; 42 | color: white; 43 | } 44 | 45 | .selectedModuleDiv { 46 | background-color: var(--main-color);/*#016262;*/ 47 | color: white; 48 | } 49 | 50 | .redDown { 51 | color: #F35A2B; 52 | } 53 | 54 | .whiteNomove { 55 | color: white; 56 | } 57 | 58 | .greenUp { 59 | color: #00FF73; 60 | } 61 | -------------------------------------------------------------------------------- /src/containers/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | import { connect } from 'react-redux' 3 | import LoginPage from './LoginPage' 4 | import DashboardPage from './DashboardPage' 5 | import '../styles/App.css' 6 | 7 | class App extends Component { 8 | static propTypes = { 9 | token: PropTypes.string.isRequired, 10 | accountNumber: PropTypes.string.isRequired, 11 | dispatch: PropTypes.func.isRequired 12 | } 13 | 14 | render() { 15 | const { token, accountNumber } = this.props 16 | let whatToDisplay = (token === "" || accountNumber === "" )? : 17 | 18 | return ( 19 |
20 | { whatToDisplay } 21 |
22 | ) 23 | } 24 | } 25 | 26 | const mapStateToProps = state => { 27 | const { tokenReducer, accountReducer } = state 28 | const { token } = tokenReducer || { token: "" } 29 | const { accountNumber } = accountReducer || { accountNumber: "" } 30 | 31 | return { token, accountNumber } 32 | } 33 | 34 | 35 | export default connect(mapStateToProps)(App) 36 | -------------------------------------------------------------------------------- /src/actions/action_earnings.js: -------------------------------------------------------------------------------- 1 | ///////////EARNINGS 2 | export const EARNINGS_ADD = 'EARNINGS_ADD' 3 | export const EARNINGS_DELETE = 'EARNINGS_DELETE' 4 | 5 | export const addEarnings = (symbol, earnings) => ({ 6 | type: EARNINGS_ADD, 7 | symbol, 8 | earnings 9 | }) 10 | 11 | export const deleteEarnings = (symbol) => ({ 12 | type: EARNINGS_DELETE, 13 | symbol 14 | }) 15 | 16 | export const askEarnings = (symbol) => (dispatch, getState) => { 17 | return fetch(`https://api.robinhood.com/marketdata/earnings/?symbol=${symbol}`, { 18 | method: 'GET', 19 | headers: new Headers({ 20 | 'Accept': 'application/json' 21 | }) 22 | }) 23 | .then(response => response.json()) 24 | .then(jsonResult => { 25 | dispatch(addEarnings(symbol, jsonResult.results)); 26 | }) 27 | .catch(function(reason) { 28 | console.log(reason); 29 | }); 30 | } 31 | 32 | export const cleanUpEarnings = () => (dispatch, getState) => { 33 | Object.keys(getState().earningsReducer.earningsAll).forEach((symbol) => { 34 | if(getState().tabsReducer.keys.indexOf(symbol) === -1) { 35 | dispatch(deleteEarnings(symbol)); 36 | } 37 | }); 38 | } 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RobInDaHood 2 | A desktop app for Robinhood 3 | 4 | ![robindahood](http://i.imgur.com/6PT1tat.jpg) 5 | 6 | This project was bootstrapped with [Create React App](https://github.com/facebookincubator/create-react-app). 7 | 8 | Project setup: https://medium.freecodecamp.com/building-an-electron-application-with-create-react-app-97945861647c#.pf5yr1mgt 9 | 10 | API reference: https://github.com/sanko/Robinhood 11 | 12 | 13 | ## How to Run 14 | ```shell 15 | npm install 16 | npm run dev 17 | ``` 18 | 19 | ## Build 20 | ```shell 21 | npm run build 22 | npm run dist 23 | ``` 24 | 25 | ## Todos 26 | - [ ] https://github.com/electron-userland/electron-builder/wiki/Auto-Update 27 | - [ ] https://github.com/ameyms/react-animated-number 28 | - [ ] portfolio chart: adjusted_close_equity vs adjusted_open_equity ***** 29 | - [ ] extended_hours order 30 | - [ ] Dividends related 31 | - [ ] Markets related 32 | - [ ] refactor the code 33 | - [ ] RH Gold related 34 | - [x] own toFixed(2) => if smaller than 1, show all digit 35 | - [ ] order: ITEK is part of the SEC's Tick Size Pilot Program. Because of this, you can only place orders in increments of $0.05. Please update to the newest version of Robinhood to make market orders for ITEK. 36 | -------------------------------------------------------------------------------- /src/styles/LeftPanelFolder.css: -------------------------------------------------------------------------------- 1 | .moduleWrapper { 2 | text-align: left; 3 | } 4 | 5 | .moduleArrow{ 6 | position: relative; 7 | background-color: none; 8 | z-index: 1; 9 | padding-left: 10px; 10 | } 11 | 12 | .folderTitle { 13 | font-size: 1.2rem; 14 | cursor: pointer; 15 | padding: 5px 0 5px 10px; 16 | background-color: black; 17 | color: white; 18 | } 19 | 20 | .open { 21 | top: -2px; 22 | width: 0.6rem; 23 | height: 0.6rem; 24 | margin-left: 0px; 25 | padding-right: 5px; 26 | transform: rotate(0deg); 27 | } 28 | 29 | .close { 30 | top: 0px; 31 | width: 0.6rem; 32 | height: 0.6rem; 33 | padding-right: 5px; 34 | transform: rotate(-90deg); 35 | } 36 | 37 | .moduleDiv { 38 | padding-left: calc( 5px + 1rem); 39 | cursor: pointer; 40 | background-color: #393C39; 41 | padding-top: 2px; 42 | padding-bottom: 2px; 43 | border-bottom: 1px solid black; 44 | color: white; 45 | } 46 | 47 | .selectedModuleDiv { 48 | background-color: var(--main-color); 49 | color: white; 50 | } 51 | 52 | .redDown { 53 | color: #F35A2B; 54 | width: 50%; 55 | } 56 | 57 | .whiteNomove { 58 | color: white; 59 | width: 50%; 60 | } 61 | 62 | .greenUp { 63 | color: #00FF73; 64 | width: 50%; 65 | } 66 | -------------------------------------------------------------------------------- /src/reducers/reducer_account.js: -------------------------------------------------------------------------------- 1 | import * as actions from '../actions'; 2 | 3 | const accountReducer = (state = { 4 | isAskingAccount: false, 5 | accountError: "", 6 | account: {}, 7 | accountNumber: "" 8 | }, action) => { 9 | switch (action.type) { 10 | case actions.ACCOUNT_RESET_ERROR: 11 | return { 12 | ...state, 13 | accountError:"", 14 | isAskingAccount: false 15 | } 16 | case actions.ACCOUNT_ASKING: 17 | return { 18 | ...state, 19 | accountError: "", 20 | isAskingAccount: true 21 | } 22 | case actions.ACCOUNT_ASKING_FAILED: 23 | return { 24 | ...state, 25 | isAskingAccount: false, 26 | accountError: action.error, 27 | account: {} 28 | } 29 | case actions.ACCOUNT_ADD: 30 | return { 31 | ...state, 32 | isAskingAccount: false, 33 | account: action.account, 34 | accountNumber: action.account.account_number 35 | } 36 | case actions.ACCOUNT_DELETE: 37 | return { 38 | ...state, 39 | account: {}, 40 | accountNumber: "" 41 | } 42 | default: 43 | return state 44 | } 45 | } 46 | 47 | export default accountReducer 48 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 16 | ROBINDAHOOD 17 | 18 | 19 |
20 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/styles/DashboardPage.css: -------------------------------------------------------------------------------- 1 | .leftPanelDiv { 2 | text-align: left; 3 | background-color: black; 4 | color: #BDBDBD; 5 | } 6 | 7 | .leftPanelSearch { 8 | position: absolute; 9 | top: 0; 10 | height: 30px; 11 | } 12 | 13 | .leftPanelRest { 14 | padding: 0px 0 10px; 15 | overflow-y: auto; 16 | height: calc(100vh - 81px); 17 | } 18 | 19 | .leftSingleDiv{ 20 | font-size: 1.2rem; 21 | padding: 5px 0 5px 25px; 22 | cursor: pointer; 23 | } 24 | 25 | .leftPanellogoutButton{ 26 | position: absolute; 27 | bottom: 0; 28 | width: 96%; 29 | left: 2%; 30 | height: 30px; 31 | border: 3px solid black; 32 | background-color: white; 33 | border-radius: 5px; 34 | box-sizing: border-box; 35 | padding-top: 5px; 36 | padding-bottom: 5px; 37 | cursor: pointer; 38 | font-weight: bold; 39 | outline: none; 40 | margin-bottom: 3px; 41 | } 42 | 43 | .leftPanellogoutButton:hover { 44 | background-color: #D8D8D8; 45 | } 46 | 47 | .logoutModalButtonsWrapper { 48 | margin: 10px 0 0 0; 49 | display: flex; 50 | justify-content:space-between; 51 | } 52 | 53 | .logoutModalButtonsWrapper >button { 54 | width: 45%; 55 | background-color: white; 56 | border: 1px solid black; 57 | border-radius: 1px; 58 | padding-top: 2px; 59 | cursor: pointer; 60 | } 61 | -------------------------------------------------------------------------------- /src/styles/LoginPage.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css?family=Francois+One'); 2 | 3 | .loginWrapper { 4 | position: relative; 5 | width: 240px; 6 | margin: 0 auto ; 7 | padding: 30px; 8 | text-align: left; 9 | background-color: black; 10 | border-radius: 5%; 11 | top: 50%; 12 | /*left: 50%;*/ 13 | transform: translate(0%, -50%); 14 | border: 1px solid white; 15 | } 16 | 17 | .loginLogo { 18 | position: absolute; 19 | top: 34px; 20 | left: 103px; 21 | width: 14px; 22 | height: 19px; 23 | background-image: url(./logo.png); 24 | background-size: 100% 100%; 25 | background-repeat: no-repeat; 26 | } 27 | 28 | .loginTitle { 29 | font-family: 'Francois One', sans-serif; 30 | font-size:2.5rem; 31 | text-align: center; 32 | padding: 20px 0 20px; 33 | color: var(--main-color);/*#EBB03F;*/ 34 | } 35 | 36 | .loginError { 37 | color: red; 38 | font-size: 1rem; 39 | height: 30px; 40 | margin: 0 10px; 41 | } 42 | 43 | .loginSubmit { 44 | width: 212px; 45 | height: 24px; 46 | margin: 10px; 47 | cursor: pointer; 48 | background-color: var(--main-color);/*#EBB03F;*/ 49 | border-width: 1px; 50 | border-style: solid; 51 | border-color: var(--main-color);/*#EBB03F;*/ 52 | border-radius: 5px; 53 | font-weight: bold; 54 | } 55 | -------------------------------------------------------------------------------- /src/reducers/reducer_token.js: -------------------------------------------------------------------------------- 1 | import * as actions from '../actions'; 2 | 3 | const tokenReducer = (state = { 4 | isAskingToken: false, 5 | tokenError: "", 6 | token: "", 7 | needMFA: false 8 | }, action) => { 9 | switch (action.type) { 10 | case actions.TOKEN_RESET_ERROR: 11 | return { 12 | ...state, 13 | tokenError:"", 14 | isAskingToken: false, 15 | needMFA: false 16 | } 17 | case actions.TOKEN_ASKING: 18 | return { 19 | ...state, 20 | tokenError:"", 21 | isAskingToken: true 22 | } 23 | case actions.TOKEN_ASKING_FAILED: 24 | return { 25 | ...state, 26 | tokenError: action.error, 27 | isAskingToken: false 28 | } 29 | case actions.TOKEN_NEED_MFA: 30 | return { 31 | ...state, 32 | tokenError: "", 33 | isAskingToken: false, 34 | needMFA: true 35 | } 36 | case actions.TOKEN_ADD: 37 | return { 38 | ...state, 39 | isAskingToken: false, 40 | token: `Token ${action.token}`, 41 | needMFA: false 42 | } 43 | case actions.TOKEN_DELETE: 44 | return { 45 | ...state, 46 | token: "" 47 | } 48 | default: 49 | return state 50 | } 51 | } 52 | 53 | export default tokenReducer 54 | -------------------------------------------------------------------------------- /src/components/Margin.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | import SectionWrapper from './SectionWrapper' 3 | import '../styles/Margin.css' 4 | 5 | class Margin extends Component { 6 | static propTypes = { 7 | ownInstrument: PropTypes.object.isRequired, 8 | buyingPower: PropTypes.number.isRequired, 9 | } 10 | 11 | render() { 12 | const { ownInstrument, buyingPower } = this.props; 13 | 14 | return ( 15 | 16 |
17 |
Initial Requirement
18 |
{`${Number(ownInstrument.margin_initial_ratio)*100}%`}
19 |
20 |
21 |
Maintenance Requirement
22 |
{`${Number(ownInstrument.maintenance_ratio)*100}%`}
23 |
24 |
25 |
{`Buying Power for ${ownInstrument.symbol}`}
26 |
{`$${(buyingPower <= 0)? 0 : buyingPower.toFixed(2)}`}
27 |
28 |
29 | ) 30 | } 31 | } 32 | 33 | 34 | export default Margin 35 | -------------------------------------------------------------------------------- /src/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux' 2 | import tokenReducer from './reducer_token' 3 | import accountReducer from './reducer_account' 4 | import watchlistsReducer from './reducer_watchlists' 5 | import positionsReducer from './reducer_positions' 6 | import instrumentsReducer from './reducer_instruments' 7 | import tabsReducer from './reducer_tabs' 8 | import fundamentalsReducer from './reducer_fundamentals' 9 | import newsReducer from './reducer_news' 10 | import quotesReducer from './reducer_quotes' 11 | import portfoliosReducer from './reducer_portfolios' 12 | import ordersReducer from './reducer_orders' 13 | import marketsReducer from './reducer_markets' 14 | import uiReducer from './reducer_ui' 15 | import localReducer from './reducer_local' 16 | import cardsReducer from './reducer_cards' 17 | import monitorReducer from './reducer_monitor' 18 | import earningsReducer from './reducer_earnings' 19 | 20 | const rootReducer = combineReducers({ 21 | tokenReducer, 22 | accountReducer, 23 | watchlistsReducer, 24 | positionsReducer, 25 | instrumentsReducer, 26 | tabsReducer, 27 | fundamentalsReducer, 28 | newsReducer, 29 | quotesReducer, 30 | portfoliosReducer, 31 | ordersReducer, 32 | marketsReducer, 33 | uiReducer, 34 | localReducer, 35 | cardsReducer, 36 | monitorReducer, 37 | earningsReducer 38 | }) 39 | 40 | export default rootReducer 41 | -------------------------------------------------------------------------------- /src/styles/PriceAlertToggle.css: -------------------------------------------------------------------------------- 1 | .priceAlertWrapper { 2 | height: 25px; 3 | text-align: center; 4 | margin: 0px 5px; 5 | position: relative; 6 | } 7 | 8 | .priceAlertWrapper > * { 9 | margin: 2px; 10 | font-size: 0.8rem; 11 | color: teal; 12 | font-weight: bold; 13 | position: relative; 14 | } 15 | 16 | 17 | 18 | 19 | /*https://www.w3schools.com/howto/howto_css_switch.asp*/ 20 | 21 | .switch { 22 | position: relative; 23 | display: inline-block; 24 | width: 40px; 25 | height: 20px; 26 | } 27 | 28 | .switch input {display:none;} 29 | 30 | .slider { 31 | position: absolute; 32 | cursor: pointer; 33 | top: 0; 34 | left: 0; 35 | right: 0; 36 | bottom: 0; 37 | background-color: #ccc; 38 | -webkit-transition: .4s; 39 | transition: .4s; 40 | } 41 | 42 | .slider:before { 43 | position: absolute; 44 | content: ""; 45 | height: 15px; 46 | width: 15px; 47 | left: 3px; 48 | bottom: 2px; 49 | background-color: white; 50 | -webkit-transition: .4s; 51 | transition: .4s; 52 | } 53 | 54 | input:checked + .slider { 55 | background-color: teal; 56 | } 57 | 58 | input:focus + .slider { 59 | box-shadow: 0 0 1px teal; 60 | } 61 | 62 | input:checked + .slider:before { 63 | transform: translateX(19px); 64 | } 65 | 66 | /* Rounded sliders */ 67 | .slider.round { 68 | border-radius: 20px; 69 | } 70 | 71 | .slider.round:before { 72 | border-radius: 50%; 73 | } 74 | -------------------------------------------------------------------------------- /src/styles/Instrument.css: -------------------------------------------------------------------------------- 1 | .instrumentWrapper { 2 | width: calc(100% - 21px); 3 | position: relative; 4 | background-color: var(--main-color); 5 | padding: 10px; 6 | overflow-y: auto; 7 | overflow-x: hidden; 8 | height: calc(100vh - 46px); 9 | } 10 | 11 | .instrumentFake { 12 | width: calc(100% - 178px); 13 | height: 10px; 14 | position: fixed; 15 | top: 26px; 16 | background-color: var(--main-color); 17 | z-index: 10; 18 | } 19 | 20 | .instrumentWrapper header { 21 | display: flex; 22 | justify-content:space-between; 23 | align-items:center; 24 | } 25 | 26 | .instrumentHWrapper { 27 | display: flex; 28 | align-items:center; 29 | justify-content:flex-start; 30 | } 31 | 32 | .instrumentH { 33 | flex: 1 1; 34 | } 35 | 36 | .instrumentH1 { 37 | font-size: 2rem; 38 | } 39 | 40 | .instrumentH2 { 41 | padding-left: 2px; 42 | } 43 | 44 | .quotesButtonsWrapper { 45 | width: 100%; 46 | display: flex; 47 | justify-content:flex-end; 48 | border-top: 2px solid #46e3c1; 49 | } 50 | 51 | .quotesButton { 52 | margin-top: 0px; 53 | margin-right: 2px; 54 | border: none; 55 | font-family: Arial; 56 | color: #045C4F; 57 | background: white; 58 | padding: 5px 10px; 59 | text-decoration: none; 60 | cursor: pointer; 61 | outline: none; 62 | } 63 | 64 | .selectedButton { 65 | background: #46e3c1; 66 | } 67 | 68 | .notTradeable { 69 | width: 100%; 70 | padding: 10px 0; 71 | background-color: grey; 72 | border-radius: 5px; 73 | color: black; 74 | text-align: center; 75 | } 76 | -------------------------------------------------------------------------------- /src/actions/action_markets.js: -------------------------------------------------------------------------------- 1 | ////////////MARKETS 2 | export const ADD_MARKET = 'ADD_MARKET' 3 | export const ADD_MARKETSHOUR = 'ADD_MARKETSHOUR' 4 | export const CLEAR_MARKETS_AND_MARKETSHOURS = 'CLEAR_MARKETS_AND_MARKETSHOURS' 5 | 6 | export const clearMarketsAndMarketsHours = () => ({ 7 | type: CLEAR_MARKETS_AND_MARKETSHOURS 8 | }) 9 | 10 | export const addMarketsHours = ( marketshour, todays_hours ) => ({ 11 | type: ADD_MARKETSHOUR, 12 | marketshour, 13 | todays_hours 14 | }) 15 | 16 | export const addMarket = (market) => ({ 17 | type: ADD_MARKET, 18 | market 19 | }) 20 | 21 | export const askMarket = (marketUrl) => (dispatch, getState) => { 22 | return fetch(marketUrl, { 23 | method: 'GET', 24 | headers: new Headers({ 25 | 'Accept': 'application/json', 26 | 'Authorization': getState().tokenReducer.token 27 | }) 28 | }) 29 | .then(response => response.json()) 30 | .then(jsonResult => { 31 | dispatch(addMarket(jsonResult)); 32 | }) 33 | .catch(function(reason) { 34 | console.log(reason); 35 | }); 36 | } 37 | 38 | export const askMarketsHour = (todays_hours) => (dispatch, getState) => { 39 | return fetch(todays_hours, { 40 | method: 'GET', 41 | headers: new Headers({ 42 | 'Accept': 'application/json', 43 | 'Authorization': getState().tokenReducer.token 44 | }) 45 | }) 46 | .then(response => response.json()) 47 | .then(jsonResult => { 48 | dispatch(addMarketsHours( jsonResult, todays_hours )); 49 | }) 50 | .catch(function(reason) { 51 | console.log(reason); 52 | }); 53 | } 54 | -------------------------------------------------------------------------------- /src/reducers/reducer_watchlists.js: -------------------------------------------------------------------------------- 1 | import * as actions from '../actions'; 2 | 3 | const watchlistsReducer = (state = { 4 | isAskingWatchlists: false, 5 | error: "", 6 | watchlists: [] 7 | }, action) => { 8 | switch (action.type) { 9 | case actions.ASKING_WATCHLISTS: 10 | return { 11 | ...state, 12 | error: "", 13 | isAskingWatchlists: true 14 | } 15 | case actions.ASKING_WATCHLISTS_FAILED: 16 | return { 17 | ...state, 18 | isAskingWatchlists: false, 19 | error: action.error, 20 | watchlists: [] 21 | } 22 | case actions.ADD_WATCHLISTS: 23 | return { 24 | ...state, 25 | isAskingWatchlists: false, 26 | watchlists: action.watchlists, 27 | } 28 | case actions.ADD_MORE_WATCHLISTS: 29 | return { 30 | ...state, 31 | isAskingWatchlists: false, 32 | watchlists: state.watchlists.concat(action.watchlists) 33 | } 34 | case actions.DELETE_WATCHLISTS: 35 | return { 36 | isAskingWatchlists: false, 37 | error: "", 38 | watchlists: [] 39 | } 40 | case actions.ADD_WATCHLIST: 41 | return { 42 | ...state, 43 | watchlists: [...state.watchlists, action.watchlist] 44 | } 45 | case actions.REMOVE_WATCHLIST: 46 | return { 47 | ...state, 48 | watchlists: [...state.watchlists.slice(0, action.instrumentIndex), ...state.watchlists.slice(action.instrumentIndex+1)] 49 | } 50 | default: 51 | return state 52 | } 53 | } 54 | 55 | export default watchlistsReducer 56 | -------------------------------------------------------------------------------- /src/actions/action_fundamentals.js: -------------------------------------------------------------------------------- 1 | ////////////FUNDAMENTALS 2 | export const FUNDAMENTAL_ADD = 'FUNDAMENTAL_ADD' 3 | export const FUNDAMENTAL_DELETE = 'FUNDAMENTAL_DELETE' 4 | export const FUNDAMENTAL_ASKING = 'FUNDAMENTAL_ASKING' 5 | export const FUNDAMENTAL_ASKING_FAILED = 'FUNDAMENTAL_ASKING_FAILED' 6 | 7 | export const askingFundamentalFailed = (error) => ({ 8 | type: FUNDAMENTAL_ASKING_FAILED, 9 | error 10 | }) 11 | 12 | export const askingFundamental = () => ({ 13 | type: FUNDAMENTAL_ASKING 14 | }) 15 | 16 | export const addFundamental = (symbol, fundamental) => ({ 17 | type: FUNDAMENTAL_ADD, 18 | symbol, 19 | fundamental 20 | }) 21 | 22 | export const deleteFundamental = (symbol) => ({ 23 | type: FUNDAMENTAL_DELETE, 24 | symbol 25 | }) 26 | 27 | export const askFundamental = (symbol) => (dispatch, getState) => { 28 | dispatch(askingFundamental()); 29 | return fetch(`https://api.robinhood.com/fundamentals/${symbol}/`, { 30 | method: 'GET', 31 | headers: new Headers({ 32 | 'Accept': 'application/json' 33 | }) 34 | }) 35 | .then(response => response.json()) 36 | .then(jsonResult => { 37 | dispatch(addFundamental(symbol, jsonResult)); 38 | }) 39 | .catch(function(reason) { 40 | console.log(reason); 41 | dispatch(askingFundamentalFailed(reason)); 42 | }); 43 | } 44 | 45 | export const cleanUpFundamentals = () => (dispatch, getState) => { 46 | Object.keys(getState().fundamentalsReducer.fundamentals).forEach((symbol) => { 47 | if(getState().tabsReducer.keys.indexOf(symbol) === -1) { 48 | dispatch(deleteFundamental(symbol)); 49 | } 50 | }); 51 | } 52 | -------------------------------------------------------------------------------- /src/reducers/reducer_portfolios.js: -------------------------------------------------------------------------------- 1 | import * as actions from '../actions'; 2 | 3 | const portfoliosReducer = (state = { 4 | quotes: { 5 | span: 'day', 6 | interval: '5minute', 7 | bounds: 'trading' 8 | }, 9 | selectedButton: '1D', 10 | portfolios: {}, 11 | historicalsPortfolios: {}, 12 | }, action) => { 13 | switch (action.type) { 14 | case actions.DELETE_PORTFOLIOS: 15 | return { 16 | quotes: { 17 | span: 'day', 18 | interval: '5minute', 19 | bounds: 'trading' 20 | }, 21 | selectedButton: '1D', 22 | portfolios: {}, 23 | historicalsPortfolios: {}, 24 | } 25 | 26 | case actions.PORTFOLIO_PAGE_SET_SELECTED_BUTTON: 27 | return Object.assign({}, state, { 28 | selectedButton: action.selectedButton 29 | }); 30 | 31 | case actions.PORTFOLIO_PAGE_UPDATE_QUOTES: 32 | return Object.assign({}, state, { 33 | quotes: action.quotes 34 | }); 35 | 36 | case actions.ADD_HIS_PORTFOLIOS: 37 | let newHistoricalsPortfolios = Object.assign({}, state.historicalsPortfolios); 38 | newHistoricalsPortfolios[action.hisType] = action.portfolios; 39 | return { 40 | ...state, 41 | historicalsPortfolios: newHistoricalsPortfolios 42 | } 43 | 44 | case actions.DELETE_HIS_PORTFOLIOS: 45 | return { 46 | ...state, 47 | } 48 | 49 | case actions.ADD_PORTFOLIOS: 50 | return { 51 | ...state, 52 | portfolios: action.portfolios 53 | } 54 | default: 55 | return state 56 | } 57 | } 58 | 59 | export default portfoliosReducer 60 | -------------------------------------------------------------------------------- /src/reducers/reducer_monitor.js: -------------------------------------------------------------------------------- 1 | import * as actions from '../actions'; 2 | 3 | const monitorReducer = (state = { 4 | tickers: {} 5 | }, action) => { 6 | switch (action.type) { 7 | case actions.MONITOR_TICKER_LAST_PRICE_CHANGE: 8 | let tempTickers = Object.assign({}, state.tickers ); 9 | tempTickers[action.instrument_id].last_price = action.last_price; 10 | return { 11 | ...state, 12 | tickers: tempTickers 13 | } 14 | case actions.MONITOR_TICKER_PERCENTAGE_CHANGE: 15 | tempTickers = Object.assign({}, state.tickers ); 16 | tempTickers[action.instrument_id].percentage = action.percentage; 17 | return { 18 | ...state, 19 | tickers: tempTickers 20 | } 21 | case actions.MONITOR_TICKER_DELETE: 22 | tempTickers = Object.assign({}, state.tickers ); 23 | delete tempTickers[action.instrument_id] 24 | return { 25 | ...state, 26 | tickers: tempTickers 27 | } 28 | case actions.MONITOR_TICKER_ADD: 29 | tempTickers = Object.assign({}, state.tickers ); 30 | tempTickers[action.instrument_id] = { 31 | instrument_id: action.instrument_id, 32 | symbol: action.symbol, 33 | percentage: action.percentage, 34 | last_price: action.last_price 35 | } 36 | return { 37 | ...state, 38 | tickers: tempTickers 39 | } 40 | default: 41 | return state 42 | } 43 | } 44 | 45 | export default monitorReducer 46 | 47 | /* 48 | symbol: { 49 | instrument_id: "instrument_id" 50 | symbol: "symbol", 51 | percentage: number, 52 | last_price: number, 53 | } 54 | */ 55 | -------------------------------------------------------------------------------- /src/reducers/reducer_tabs.js: -------------------------------------------------------------------------------- 1 | import * as actions from '../actions'; 2 | 3 | const NOTABKEY = "noTAbKey" 4 | 5 | const tabsReducer = (state = { 6 | selectedKey: NOTABKEY, 7 | keys: [], 8 | tabs: {} 9 | }, action) => { 10 | switch (action.type) { 11 | case actions.TABS_DELETE_ALL: 12 | return { 13 | selectedKey: NOTABKEY, 14 | keys: [], 15 | tabs: {} 16 | } 17 | case actions.TAB_ADD: 18 | let tempTabObject = {}; 19 | tempTabObject[action.currentKey] = action.newTab; 20 | return { 21 | ...state, 22 | selectedKey: action.currentKey, 23 | keys: state.keys.concat(action.currentKey), 24 | tabs: Object.assign({}, state.tabs, tempTabObject) 25 | } 26 | case actions.TAB_DELETE: 27 | let newTabs = Object.assign({}, state.tabs); 28 | delete newTabs[action.deletedKey] 29 | let realKey = (action.currentKeys.length===0)? NOTABKEY : state.selectedKey; 30 | return { 31 | ...state, 32 | selectedKey: realKey, 33 | keys: action.currentKeys, 34 | tabs: newTabs 35 | } 36 | case actions.TAB_SELECT: 37 | let newSelectedKey = action.currentKey || NOTABKEY; 38 | return { 39 | ...state, 40 | selectedKey: newSelectedKey 41 | } 42 | case actions.TAB_REORDER: 43 | return { 44 | ...state, 45 | keys: action.currentKeys 46 | } 47 | default: 48 | return state 49 | } 50 | } 51 | 52 | export default tabsReducer 53 | 54 | /* 55 | key = data.symbol 56 | newTab = { 57 | key: key, 58 | title: data.symbol, 59 | instrument: data.instrument, 60 | type: data.type 61 | } 62 | */ 63 | -------------------------------------------------------------------------------- /src/reducers/reducer_quotes.js: -------------------------------------------------------------------------------- 1 | import * as actions from '../actions'; 2 | 3 | const quotesReducer = (state = { 4 | historicalsQuotes: {}, 5 | quotes: {} 6 | }, action) => { 7 | switch (action.type) { 8 | case actions.ADD_HIS_QUOTES: 9 | let newHistoricalsQuotes = Object.assign({}, state.historicalsQuotes); 10 | newHistoricalsQuotes[ action.symbol+action.hisType ] = action.quotes; 11 | return { 12 | ...state, 13 | historicalsQuotes: newHistoricalsQuotes 14 | } 15 | case actions.DELETE_HIS_QUOTES: 16 | let tempHistoricalsQuotes = Object.assign({}, state.historicalsQuotes); 17 | delete tempHistoricalsQuotes[action.symbolHisType]; 18 | return { 19 | ...state, 20 | historicalsQuotes: tempHistoricalsQuotes 21 | } 22 | case actions.ADD_QUOTE: 23 | let newQuotes = Object.assign({}, state.quotes); 24 | newQuotes[action.symbol] = action.quote; 25 | return { 26 | ...state, 27 | quotes: newQuotes 28 | } 29 | case actions.DELETE_QUOTE: 30 | let tempQuotes = Object.assign({}, state.quotes); 31 | delete tempQuotes[action.symbol]; 32 | return { 33 | ...state, 34 | quotes: tempQuotes 35 | } 36 | case actions.ADD_MULTIPLE_QUOTES: 37 | let tempQuotesObj = {}; 38 | action.quotesArray.forEach((quote)=>{ 39 | if(quote !== null){ 40 | tempQuotesObj[quote.symbol] = quote; 41 | } 42 | }) 43 | return { 44 | ...state, 45 | quotes: Object.assign({}, state.quotes, tempQuotesObj) 46 | } 47 | default: 48 | return state 49 | } 50 | } 51 | 52 | export default quotesReducer 53 | -------------------------------------------------------------------------------- /src/actions/action_instruments.js: -------------------------------------------------------------------------------- 1 | import { askQuote } from './action_quotes' 2 | ////////////INSTRUMENTS 3 | export const ADD_INSTRUMENT = 'ADD_INSTRUMENT' 4 | export const DELETE_INSTRUMENT = 'DELETE_INSTRUMENT' 5 | export const ASKING_INSTRUMENT = 'ASKING_INSTRUMENT' 6 | export const ASKING_INSTRUMENT_FAILED = 'ASKING_INSTRUMENT_FAILED' 7 | 8 | export const askingInstrumentFailed = (error) => ({ 9 | type: ASKING_INSTRUMENT_FAILED, 10 | error 11 | }) 12 | 13 | export const askingInstrument = () => ({ 14 | type: ASKING_INSTRUMENT 15 | }) 16 | 17 | export const addInstrument = instrument => ({ 18 | type: ADD_INSTRUMENT, 19 | instrument 20 | }) 21 | 22 | export const deleteInstrument = (instrumentUrl) => ({ 23 | type: DELETE_INSTRUMENT, 24 | instrumentUrl 25 | }) 26 | 27 | export const askInstrument = (instrument) => (dispatch, getState) => { 28 | dispatch(askingInstrument()); 29 | return fetch(instrument, { 30 | method: 'GET', 31 | headers: new Headers({ 32 | 'Accept': 'application/json' 33 | }) 34 | }) 35 | .then(response => response.json()) 36 | .then(jsonResult => { 37 | dispatch(addInstrument(jsonResult)); 38 | dispatch(askQuote(jsonResult.symbol)); 39 | }) 40 | .catch(function(reason) { 41 | console.log(reason); 42 | dispatch(askingInstrumentFailed(reason)); 43 | }); 44 | } 45 | 46 | export const cleanUpInstruments = () => (dispatch, getState) => { 47 | Object.keys(getState().instrumentsReducer.instruments).forEach((instrumentUrl) => { 48 | if(getState().tabsReducer.keys.indexOf(getState().instrumentsReducer.instruments[instrumentUrl].symbol) === -1) { 49 | dispatch(deleteInstrument(instrumentUrl)); 50 | } 51 | }); 52 | } 53 | -------------------------------------------------------------------------------- /src/styles/OrderDetail.css: -------------------------------------------------------------------------------- 1 | .orderDetailLoadingWrapper{ 2 | background-color: black; 3 | color: white; 4 | text-align: center; 5 | padding-top: 10px; 6 | width: 100%; 7 | } 8 | 9 | .orderDetailWrapper { 10 | background-color: black; 11 | } 12 | 13 | .orderDetailWrapper > header { 14 | background-color: var(--main-color); 15 | padding: 2%; 16 | text-align: left; 17 | position: relative; 18 | } 19 | 20 | .orderDetailWrapper > header > h6{ 21 | font-size: 0.8rem; 22 | margin-bottom: 2px; 23 | position: relative; 24 | width: 90%; 25 | left: 5%; 26 | } 27 | 28 | .orderDetailWrapper > header > h2{ 29 | font-size: 1.1rem; 30 | position: relative; 31 | width: 90%; 32 | left: 5%; 33 | } 34 | 35 | .OrderDetailUlWrapper { 36 | width: 100%; 37 | height: 100%; 38 | } 39 | 40 | .OrderDetailUlWrapper > ul { 41 | position: relative; 42 | width: 80%; 43 | left: 10%; 44 | padding: 2% 0; 45 | text-align: left; 46 | } 47 | 48 | .OrderDetailUlWrapper > ul > li { 49 | margin-bottom: 12px; 50 | } 51 | 52 | .OrderDetailUlWrapper > ul > li:last-child { 53 | margin-bottom: 0px; 54 | } 55 | 56 | .orderDetailType { 57 | font-size: 0.8rem; 58 | color: #BDBDBD; 59 | margin-bottom: 2px; 60 | } 61 | 62 | .cancelOrderButton { 63 | margin-top: 10px; 64 | border: none; 65 | border-radius: 5px; 66 | padding: 5px 10px; 67 | background-color: red; 68 | color: white; 69 | font-weight: bold; 70 | cursor: pointer; 71 | } 72 | 73 | .cancelOrderButton:hover { 74 | background-color: #D9524E; 75 | } 76 | 77 | .cancelOrderDiv { 78 | margin-top: 10px; 79 | border: none; 80 | border-radius: 5px; 81 | padding: 5px 10px; 82 | background-color: #D9524E; 83 | color: white; 84 | font-weight: bold; 85 | } 86 | -------------------------------------------------------------------------------- /src/components/LeftPanelModule.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | import '../styles/LeftPanelModule.css' 3 | import arrow from '../styles/arrow.png'; 4 | import LeftPanelFolder from './LeftPanelFolder' 5 | 6 | class LeftPanelModule extends Component { 7 | static propTypes = { 8 | quotes: PropTypes.object.isRequired, 9 | moduleOpen: PropTypes.bool.isRequired, 10 | toggleModule: PropTypes.func.isRequired, 11 | moduleName: PropTypes.string.isRequired, 12 | listsData: PropTypes.array.isRequired, 13 | selectedKey: PropTypes.string.isRequired, 14 | callback: PropTypes.func.isRequired 15 | } 16 | 17 | openClose = () => { 18 | this.props.toggleModule(); 19 | } 20 | 21 | render() { 22 | let { quotes, listsData, callback, selectedKey, localLists, toggleLocallist, moduleOpen } =this.props; 23 | 24 | let data = listsData.map((listData,index) => { 25 | return (toggleLocallist(index)} 35 | />) 36 | }) 37 | 38 | return ( 39 |
40 |
41 | {moduleOpen? 44 | {this.props.moduleName} 45 |
46 | {moduleOpen? data : null} 47 |
48 | ) 49 | } 50 | } 51 | 52 | export default LeftPanelModule 53 | -------------------------------------------------------------------------------- /src/containers/PriceAlertToggle.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | import { connect } from 'react-redux' 3 | import { 4 | deleteMonitorTicker, 5 | addMonitorTicker 6 | } from '../actions' 7 | import '../styles/PriceAlertToggle.css' 8 | 9 | class PriceAlertToggle extends Component { 10 | static propTypes = { 11 | symbol: PropTypes.string.isRequired, 12 | last_price: PropTypes.number.isRequired, 13 | instrument_id: PropTypes.string.isRequired 14 | } 15 | 16 | handleTogglePriceAlert = (event) => { 17 | const target = event.target; 18 | const value = target.type === 'checkbox' ? target.checked : target.value; 19 | this.props.onToggleMonitorTicker(value); 20 | } 21 | 22 | render() { 23 | const { ticker } = this.props; 24 | const isIn = (ticker)? true : false; 25 | 26 | return ( 27 |
28 |
PRICE ALERT
29 | 38 |
39 | ) 40 | } 41 | } 42 | 43 | const mapStateToProps = ({ monitorReducer }, ownProps) => { 44 | const { tickers } = monitorReducer; 45 | return { ticker: tickers[ownProps.instrument_id] }; 46 | } 47 | 48 | const mapDispatchToProps = (dispatch, ownProps) => ({ 49 | onToggleMonitorTicker: (value) => { 50 | if(value) { 51 | dispatch(addMonitorTicker(ownProps.instrument_id, ownProps.symbol, 3, ownProps.last_price)); 52 | } 53 | else { 54 | dispatch(deleteMonitorTicker(ownProps.instrument_id)); 55 | } 56 | } 57 | }) 58 | 59 | export default connect(mapStateToProps, mapDispatchToProps)(PriceAlertToggle) 60 | -------------------------------------------------------------------------------- /src/components/PriceAlertTicker.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | import NumericInput from 'react-numeric-input'; 3 | NumericInput.style.input.width = '65px'; 4 | NumericInput.style.input.height = '25px'; 5 | import '../styles/PriceAlertPage.css' 6 | 7 | class PriceAlertTicker extends Component { 8 | static propTypes = { 9 | ticker: PropTypes.object.isRequired 10 | } 11 | 12 | percentageFormat = (num) => { 13 | return num + '%'; 14 | } 15 | 16 | priceFormat = (num) => { 17 | return '$' + num; 18 | } 19 | 20 | render() { 21 | const { 22 | ticker, 23 | onChangeMonitorTickerPercentage, 24 | onChangeMonitorTickerLastPrice, 25 | onDeleteMonitorTicker 26 | } = this.props; 27 | const { instrument_id, percentage, last_price, symbol} = ticker; 28 | 29 | return ( 30 |
31 |
{`$${symbol}`}
32 |
33 | {onChangeMonitorTickerPercentage(instrument_id, number);}} 39 | /> 40 | of 41 | {onChangeMonitorTickerLastPrice(instrument_id, number);}} 48 | /> 49 |
50 |
51 | 54 |
55 |
56 | ) 57 | } 58 | } 59 | 60 | export default PriceAlertTicker 61 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ROBINDAHOOD", 3 | "version": "1.0.3", 4 | "license": "MIT", 5 | "description": "A Robinhood desktop app", 6 | "author": "Ed Lai ", 7 | "private": true, 8 | "devDependencies": { 9 | "electron": "^1.6.2", 10 | "electron-builder": "^16.8.3", 11 | "electron-builder-squirrel-windows": "^16.8.3", 12 | "foreman": "^2.0.0", 13 | "react-router-dom": "^4.0.0", 14 | "react-scripts": "0.9.5" 15 | }, 16 | "dependencies": { 17 | "lodash": "^4.17.4", 18 | "material-ui": "^0.17.1", 19 | "node-notifier": "^5.1.2", 20 | "normalizr": "^3.2.2", 21 | "react": "^15.4.2", 22 | "react-autosuggest": "^9.0.0", 23 | "react-dnd": "^2.3.0", 24 | "react-dnd-html5-backend": "^2.3.0", 25 | "react-dom": "^15.4.2", 26 | "react-draggable-tab": "^0.8.1", 27 | "react-edit-inline": "^1.0.8", 28 | "react-modal": "^1.7.3", 29 | "react-numeric-input": "^2.0.7", 30 | "react-redux": "^5.0.3", 31 | "recharts": "^0.21.2", 32 | "redux": "^3.6.0", 33 | "redux-action-buffer": "^1.0.1", 34 | "redux-persist": "^4.4.2", 35 | "redux-thunk": "^2.2.0" 36 | }, 37 | "main": "src/electron-starter.js", 38 | "homepage": "./", 39 | "scripts": { 40 | "start": "react-scripts start", 41 | "build": "react-scripts build", 42 | "test": "react-scripts test --env=jsdom", 43 | "eject": "react-scripts eject", 44 | "electron": "electron .", 45 | "dev": "nf start", 46 | "pack": "build --dir", 47 | "dist": "build", 48 | "postinstall": "install-app-deps" 49 | }, 50 | "build": { 51 | "appId": "com.electron.robindahood", 52 | "asarUnpack": "./node_modules/node-notifier/vendor/**", 53 | "nsis": { 54 | "deleteAppDataOnUninstall": true 55 | }, 56 | "directories": { 57 | "buildResources" : "buildResources" 58 | }, 59 | "win": { 60 | "target": "NSIS", 61 | "icon": "./icon.ico" 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { render } from 'react-dom' 3 | import { createStore, applyMiddleware, compose } from 'redux' 4 | import { Provider } from 'react-redux' 5 | import thunk from 'redux-thunk' 6 | import {persistStore, autoRehydrate} from 'redux-persist' 7 | import {REHYDRATE} from 'redux-persist/constants' 8 | import createActionBuffer from 'redux-action-buffer' 9 | //import { BrowserRouter as Router, Route } from 'react-router-dom' 10 | import reducer from './reducers' 11 | 12 | import App from './containers/App' 13 | import './styles/App.css' 14 | 15 | /* 16 | Number.prototype.myFixed = function(digits){ 17 | if( Number(this) < 1 ) { 18 | return Number.prototype.toFixed.call(this, 4); 19 | } 20 | 21 | return Number.prototype.toFixed.call(this, digits); 22 | } 23 | */ 24 | 25 | const middleware = [ thunk ] 26 | /* 27 | if (process.env.NODE_ENV !== 'production') { 28 | middleware.push(createLogger()) 29 | } 30 | */ 31 | let enhancer = compose( 32 | autoRehydrate(), 33 | applyMiddleware( 34 | createActionBuffer(REHYDRATE) //make sure to apply this after redux-thunk et al. 35 | ) 36 | ) 37 | 38 | const store = createStore( 39 | reducer, 40 | compose( 41 | applyMiddleware(...middleware), 42 | enhancer 43 | ) 44 | ) 45 | // begin periodically persisting the store 46 | //persistStore(store) 47 | 48 | class AppProvider extends Component { 49 | constructor() { 50 | super() 51 | this.state = { rehydrated: false } 52 | } 53 | 54 | componentWillMount(){ 55 | persistStore(store, {}, () => { 56 | this.setState({ rehydrated: true }) 57 | }) 58 | } 59 | 60 | render() { 61 | if(!this.state.rehydrated){ 62 | return ( 63 |
64 | Loading... 65 |
66 | ) 67 | } 68 | return ( 69 | 70 | 71 | 72 | ) 73 | } 74 | } 75 | 76 | render( 77 | , 78 | document.getElementById('root') 79 | ) 80 | -------------------------------------------------------------------------------- /src/containers/PriceAlertPage.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | import { connect } from 'react-redux' 3 | import { 4 | deleteMonitorTicker, 5 | changeMonitorTickerPercentage, 6 | changeMonitorTickerLastPrice 7 | } from '../actions' 8 | import PriceAlertTicker from '../components/PriceAlertTicker' 9 | import '../styles/PriceAlertPage.css' 10 | 11 | class PriceAlertPage extends Component { 12 | 13 | static propTypes = { 14 | isCurrent: PropTypes.bool.isRequired 15 | } 16 | 17 | render() { 18 | const { 19 | isCurrent, 20 | tickers, 21 | onChangeMonitorTickerPercentage, 22 | onChangeMonitorTickerLastPrice, 23 | onDeleteMonitorTicker 24 | } = this.props; 25 | //show null if not current page 26 | if(!isCurrent){ return null; } 27 | 28 | let allTickers = Object.keys(tickers).sort( (a, b) => { 29 | var nameA = tickers[a].symbol.toUpperCase(); 30 | var nameB = tickers[b].symbol.toUpperCase(); 31 | if (nameA < nameB) { return -1; } 32 | if (nameA > nameB) { return 1; } 33 | return 0; 34 | }).map((key) => ( 35 |
  • 36 | 42 |
  • 43 | )); 44 | 45 | return ( 46 |
    47 |
    48 |
    49 |
    50 |

    Price Alert

    51 |
    52 |
    53 |
      54 | { allTickers } 55 |
    56 |
    57 | ) 58 | } 59 | } 60 | 61 | const mapStateToProps = ({ monitorReducer }) => { 62 | const { tickers } = monitorReducer; 63 | return { tickers }; 64 | } 65 | 66 | const mapDispatchToProps = (dispatch, ownProps) => ({ 67 | onChangeMonitorTickerPercentage: (instrument_id, percentage) => { 68 | dispatch(changeMonitorTickerPercentage(instrument_id, percentage)); 69 | }, 70 | onChangeMonitorTickerLastPrice: (instrument_id, last_price) => { 71 | dispatch(changeMonitorTickerLastPrice(instrument_id, last_price)); 72 | }, 73 | onDeleteMonitorTicker: (instrument_id) => { 74 | dispatch(deleteMonitorTicker(instrument_id)); 75 | } 76 | }) 77 | 78 | export default connect(mapStateToProps, mapDispatchToProps)(PriceAlertPage) 79 | -------------------------------------------------------------------------------- /src/components/LeftPanelFolder.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | 3 | import '../styles/LeftPanelFolder.css' 4 | import arrow from '../styles/arrow.png'; 5 | import LeftPanelItem from '../components/LeftPanelItem' 6 | import { displayPercentage } from '../utils' 7 | 8 | class LeftPanelFolder extends Component { 9 | static propTypes = { 10 | moduleName: PropTypes.string.isRequired, 11 | moduleData: PropTypes.array.isRequired, 12 | selectedKey: PropTypes.string.isRequired, 13 | callback: PropTypes.func.isRequired, 14 | toggleLocallist: PropTypes.func.isRequired, 15 | quotes: PropTypes.object.isRequired 16 | } 17 | 18 | handleClick = (id) => { 19 | this.props.callback(this.props.moduleData[id]); 20 | } 21 | 22 | openClose = () => { 23 | this.props.toggleLocallist(); 24 | } 25 | 26 | render() { 27 | let { quotes } =this.props; 28 | 29 | let data = this.props.moduleData.map((instrument, index) => { 30 | if(!quotes[instrument.symbol]) return null; 31 | 32 | let last_price = (quotes[instrument.symbol].last_extended_hours_trade_price)? quotes[instrument.symbol].last_extended_hours_trade_price : quotes[instrument.symbol].last_trade_price; 33 | let colorClassName = (Number(last_price) > Number(quotes[instrument.symbol].previous_close))? 34 | "greenUp" : (Number(last_price) === Number(quotes[instrument.symbol].previous_close))? 35 | "whiteNomove":"redDown"; 36 | 37 | return( 38 | this.handleClick(index)} 43 | className={this.props.selectedKey === instrument.symbol? "moduleDiv selectedModuleDiv" : "moduleDiv"} 44 | > 45 |
    46 | { `$${Number(last_price).toFixed(2)}` } 47 |
    48 |
    49 | { displayPercentage(last_price, quotes[instrument.symbol].previous_close) } 50 |
    51 |
    52 | ); 53 | }); 54 | 55 | let open = this.props.open; 56 | 57 | return ( 58 |
    59 |
    60 | {open? 63 | {this.props.moduleName} 64 |
    65 | {open? data : null} 66 |
    67 | ) 68 | } 69 | } 70 | 71 | export default LeftPanelFolder 72 | -------------------------------------------------------------------------------- /src/styles/App.css: -------------------------------------------------------------------------------- 1 | /* http://meyerweb.com/eric/tools/css/reset/ 2 | v2.0 | 20110126 3 | License: none (public domain) 4 | */ 5 | html, body, div, span, applet, object, iframe, 6 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 7 | a, abbr, acronym, address, big, cite, code, 8 | del, dfn, em, img, ins, kbd, q, s, samp, 9 | small, strike, strong, sub, sup, tt, var, 10 | b, u, i, center, 11 | dl, dt, dd, ol, ul, li, 12 | fieldset, form, label, legend, 13 | table, caption, tbody, tfoot, thead, tr, th, td, 14 | article, aside, canvas, details, embed, 15 | figure, figcaption, footer, header, hgroup, 16 | menu, nav, output, ruby, section, summary, 17 | time, mark, audio, video { 18 | margin: 0; 19 | padding: 0; 20 | border: 0; 21 | font-size: 100%; 22 | font: inherit; 23 | vertical-align: baseline; 24 | } 25 | /* HTML5 display-role reset for older browsers */ 26 | article, aside, details, figcaption, figure, 27 | footer, header, hgroup, menu, nav, section { 28 | display: block; 29 | } 30 | body { 31 | line-height: 1; 32 | } 33 | ol, ul { 34 | list-style: none; 35 | } 36 | blockquote, q { 37 | quotes: none; 38 | } 39 | blockquote:before, blockquote:after, 40 | q:before, q:after { 41 | content: ''; 42 | content: none; 43 | } 44 | table { 45 | border-collapse: collapse; 46 | border-spacing: 0; 47 | } 48 | /*****************************************************/ 49 | @import url('https://fonts.googleapis.com/css?family=Roboto'); 50 | 51 | html, body { 52 | width: 100%; 53 | height: 100%; 54 | background-color: #1C1C1C; 55 | color: white; 56 | text-align: center; 57 | font-family: 'Roboto', sans-serif; 58 | font-size: 16px; 59 | cursor: default; 60 | overflow: hidden; 61 | /* 62 | background-image: url('./logo.png'); 63 | background-repeat: no-repeat; 64 | background-position: calc(50% + 61px) calc(50% - 19px); 65 | */ 66 | background-image: url('./onTheMoon1920x1200.jpg'); 67 | background-repeat: no-repeat; 68 | background-position: bottom; 69 | background-size: cover; 70 | 71 | } 72 | 73 | :root { 74 | --main-color: #40C9BD; 75 | } 76 | 77 | .AppProvider { 78 | background-color: var(--main-color); 79 | height: 100vh; 80 | line-height: 100vh; 81 | } 82 | 83 | .elephantprint { 84 | position: relative; 85 | /*background-image: url('./eprint_1024x1024.png');*/ 86 | background-size:cover; 87 | width: 100vw; 88 | height: 100vh; 89 | } 90 | 91 | ::-webkit-scrollbar { 92 | width: 7px; 93 | background-color: #46e3c1; 94 | } 95 | 96 | ::-webkit-scrollbar-track { 97 | 98 | } 99 | 100 | ::-webkit-scrollbar-thumb { 101 | background-color: teal; 102 | } 103 | -------------------------------------------------------------------------------- /src/containers/EditableLocalPositions.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { connect } from 'react-redux' 3 | 4 | import SectionWrapper from '../components/SectionWrapper' 5 | import ListContainer from '../components/ListContainer' 6 | 7 | import { 8 | reorderLocalPosition, 9 | addLocalPositionFolder, 10 | deleteLocalPositionFolder, 11 | reorderLocalPositions, 12 | renameLocalPositionFolder, 13 | } from '../actions' 14 | 15 | import '../styles/News.css' 16 | 17 | class EditableLocalPositions extends Component { 18 | 19 | addFolder = () => { 20 | const { onAddFolder, localPositions } = this.props; 21 | onAddFolder(localPositions.length); 22 | } 23 | 24 | render() { 25 | const { 26 | localPositions, 27 | instruments, 28 | // position handlers 29 | onDeleteFolder, 30 | onReorderPosition, 31 | onReorderLocalPosition, 32 | onRenameLocalPosition 33 | } = this.props; 34 | 35 | return ( 36 | 37 |
    38 |
    Positions
    39 | 45 |
    46 | 55 |
    56 | ) 57 | } 58 | } 59 | 60 | const mapStateToProps = ({ localReducer, instrumentsReducer }, ownProps) => { 61 | const { localPositions = [] } = localReducer; 62 | const { instruments = {} } = instrumentsReducer; 63 | 64 | return { localPositions, instruments }; 65 | } 66 | 67 | const mapDispatchToProps = (dispatch, ownProps) => ({ 68 | onAddFolder: (folderIndex) => { 69 | dispatch(addLocalPositionFolder(`Folder ${folderIndex}`, [])); 70 | }, 71 | onDeleteFolder: (index, position ) => { 72 | dispatch(deleteLocalPositionFolder(index)); 73 | }, 74 | onReorderPosition: (positionIndex, position ) => { 75 | dispatch(reorderLocalPosition(positionIndex, position)); 76 | }, 77 | onReorderLocalPosition: (aI, bI) => { 78 | dispatch(reorderLocalPositions(aI, bI)); 79 | }, 80 | onRenameLocalPosition: (index, name) => { 81 | dispatch(renameLocalPositionFolder(index, name)); 82 | } 83 | }) 84 | 85 | export default connect(mapStateToProps, mapDispatchToProps)(EditableLocalPositions) 86 | -------------------------------------------------------------------------------- /src/containers/News.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | import { connect } from 'react-redux' 3 | import Modal from 'react-modal' 4 | import SectionWrapper from '../components/SectionWrapper' 5 | import NewsCard from '../components/NewsCard' 6 | import { askNews } from '../actions' 7 | import { printDate } from '../utils' 8 | import '../styles/News.css' 9 | 10 | const electron = window.require('electron'); 11 | const shell = electron.shell; 12 | 13 | const customStyles = { 14 | content : { 15 | top : '50px', 16 | backgroundColor : 'teal', 17 | textAlign : 'left', 18 | padding : '10px' 19 | }, 20 | overlay :{ zIndex: 999 } 21 | }; 22 | 23 | class News extends Component { 24 | static propTypes = { 25 | symbol: PropTypes.string.isRequired 26 | } 27 | 28 | constructor(props) { 29 | super(props); 30 | this.state = { modalIsOpen: false}; 31 | } 32 | 33 | componentDidMount() { 34 | this.props.onFetchNews(); 35 | } 36 | 37 | toggleModal = () => { 38 | this.setState({modalIsOpen: !this.state.modalIsOpen}); 39 | } 40 | 41 | openUrlInBrowser = (e) => { 42 | e.preventDefault(); 43 | shell.openExternal(e.target.href); 44 | } 45 | 46 | render() { 47 | const news = this.props.news; 48 | if(!news || news.results.length === 0) return null; 49 | 50 | let allNews = news.results.map((eachNews, index) => ( 51 | 58 | )); 59 | 60 | return ( 61 | 62 | { allNews.slice(0, 3) } 63 | { (allNews.length > 3)? ( 64 |
    65 | 68 |
    69 | ) : null} 70 | 76 | {allNews} 77 | 78 |
    79 | ) 80 | } 81 | } 82 | 83 | const mapStateToProps = ({ newsReducer }, ownProps) => { 84 | const { newsAll } = newsReducer; 85 | return { news: newsAll[ownProps.symbol] }; 86 | } 87 | 88 | const mapDispatchToProps = (dispatch, ownProps) => ({ 89 | onFetchNews: () => { 90 | dispatch(askNews(ownProps.symbol)); 91 | } 92 | }) 93 | 94 | export default connect(mapStateToProps, mapDispatchToProps)(News) 95 | -------------------------------------------------------------------------------- /src/components/Dashboard.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import '../styles/Dashboard.css' 3 | 4 | class Dashboard extends Component { 5 | constructor(props) { 6 | super(props); 7 | this.state={ 8 | isResizing:false, 9 | lastDownX:0, 10 | rightWidth: "" 11 | }; 12 | } 13 | 14 | mousedown = (e) => { 15 | this.setState({ 16 | isResizing : true, 17 | lastDownX : e.clientX 18 | }); 19 | } 20 | 21 | mousemove = (e) => { 22 | e.preventDefault(); 23 | if (!this.state.isResizing){ 24 | return; 25 | } 26 | 27 | if(e.clientX > 150 || e.clientX<150){ 28 | return; 29 | } 30 | else { 31 | this.dbLeftPanel.style.width = e.clientX + "px"; 32 | this.dbRightPanel.style.width = (this.dbContainer.getBoundingClientRect().width-e.clientX) + "px"; 33 | this.dbRightPanel.style.left = e.clientX + "px"; 34 | this.setState({rightWidth: this.dbRightPanel.getBoundingClientRect().width + "px"}) 35 | } 36 | } 37 | 38 | mouseup = (e) => { 39 | this.setState({isResizing : false}); 40 | } 41 | 42 | resize = (e) => { 43 | //document.body.clientWidth; 44 | //console.log(document.body.clientWidth); 45 | //console.log(this.dbContainer.getBoundingClientRect().width); 46 | //console.log(this.dbLeftPanel.getBoundingClientRect().width); 47 | this.setState({rightWidth: this.dbContainer.getBoundingClientRect().width - this.dbLeftPanel.getBoundingClientRect().width + "px"}) 48 | } 49 | 50 | componentDidMount(){ 51 | //this.dbDrag.addEventListener('mousedown', this.mousedown); 52 | //document.addEventListener('mousemove', this.mousemove); 53 | //document.addEventListener('mouseup', this.mouseup); 54 | //window.addEventListener('resize', this.resize); 55 | } 56 | 57 | componentWillUnmount(){ 58 | //this.dbDrag.removeEventListener('mousedown', this.mousedown); 59 | //document.removeEventListener('mousemove', this.mousemove); 60 | //document.removeEventListener('mouseup', this.mouseup); 61 | //window.removeEventListener('resize', this.resize); 62 | } 63 | 64 | render() { 65 | const children = React.Children.toArray(this.props.children); 66 | const left = children[0]; 67 | const right = children[1]; 68 | 69 | return ( 70 |
    { this.dbContainer = div; }}> 71 |
    { this.dbLeftPanel = div; }}> 72 | {left} 73 |
    74 |
    { this.dbRightPanel = div; }}> 75 |
    { this.dbDrag = div; }}>
    76 | {React.cloneElement(right, {width: this.state.rightWidth})} 77 |
    78 |
    79 | ) 80 | } 81 | } 82 | 83 | export default Dashboard 84 | -------------------------------------------------------------------------------- /src/containers/EditableLocalWatchLists.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { connect } from 'react-redux' 3 | 4 | import SectionWrapper from '../components/SectionWrapper' 5 | import ListContainer from '../components/ListContainer' 6 | 7 | import { 8 | reorderLocalWatchlist, 9 | addLocalWatchlistFolder, 10 | deleteLocalWatchlistFolder, 11 | reorderLocalWatchlists, 12 | renameLocalWatchlistFolder, 13 | } from '../actions' 14 | 15 | import '../styles/News.css' 16 | 17 | class EditableLocalWatchLists extends Component { 18 | 19 | addFolder = () => { 20 | const { onAddWatchListFolder, localWatchlists } = this.props; 21 | onAddWatchListFolder(localWatchlists.length); 22 | } 23 | 24 | render() { 25 | const { 26 | localWatchlists, 27 | localPositions, 28 | instruments, 29 | // watchlists handlers 30 | onDeleteWatchListFolder, 31 | onReorderWatchList, 32 | onReorderLocalWatchList, 33 | onRenameWatchListFolder 34 | } = this.props; 35 | 36 | return ( 37 | 38 |
    39 |
    Watchlists
    40 | 46 |
    47 | 56 |
    57 | ) 58 | } 59 | } 60 | 61 | const mapStateToProps = ({ localReducer, instrumentsReducer }, ownProps) => { 62 | const { localWatchlists = [], localPositions = [] } = localReducer; 63 | const { instruments = {} } = instrumentsReducer; 64 | 65 | return { localWatchlists, localPositions, instruments }; 66 | } 67 | 68 | const mapDispatchToProps = (dispatch, ownProps) => ({ 69 | onAddWatchListFolder: (folderIndex) => { 70 | dispatch(addLocalWatchlistFolder(`Folder ${folderIndex}`, [])); 71 | }, 72 | onDeleteWatchListFolder: (index) => { 73 | dispatch(deleteLocalWatchlistFolder(index)); 74 | }, 75 | onReorderWatchList: (watchlistIndex, watchlist) => { 76 | dispatch(reorderLocalWatchlist(watchlistIndex, watchlist)); 77 | }, 78 | onReorderLocalWatchList: (aI, bI) => { 79 | dispatch(reorderLocalWatchlists(aI, bI)); 80 | }, 81 | onRenameWatchListFolder: (index, name) => { 82 | dispatch(renameLocalWatchlistFolder(index, name)); 83 | } 84 | }) 85 | 86 | export default connect(mapStateToProps, mapDispatchToProps)(EditableLocalWatchLists) 87 | -------------------------------------------------------------------------------- /src/containers/Statistics.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | import { connect } from 'react-redux' 3 | import { askFundamental } from '../actions' 4 | import SectionWrapper from '../components/SectionWrapper' 5 | import StatisticsCard from '../components/StatisticsCard' 6 | import '../styles/Statistics.css' 7 | import { carry } from '../utils' 8 | 9 | class Statistics extends Component { 10 | static propTypes = { 11 | symbol: PropTypes.string.isRequired 12 | } 13 | 14 | constructor(props) { 15 | super(props); 16 | this.twoMinuteInterval = undefined; 17 | } 18 | 19 | componentDidMount() { 20 | this.props.onFetchFundamental(); 21 | // Start the 2 minutes poller to requery for data 22 | this.startStatisticsPoller(); 23 | } 24 | 25 | componentWillUnmount() { 26 | this.clearStatisticsPoller(); 27 | } 28 | 29 | twoMinutesJobs = () => { 30 | this.props.onFetchFundamental(); 31 | } 32 | 33 | startStatisticsPoller = () => { 34 | this.twoMinuteInterval = setInterval(this.twoMinutesJobs, 120000); 35 | } 36 | 37 | clearStatisticsPoller = () => { 38 | clearInterval(this.twoMinuteInterval); 39 | } 40 | 41 | render() { 42 | const fundamental = this.props.fundamental; 43 | if(!fundamental) return null; 44 | 45 | return ( 46 | 47 |
    48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 |
    59 |
    60 | ) 61 | } 62 | } 63 | 64 | const mapStateToProps = ({ fundamentalsReducer }, ownProps) => { 65 | const { fundamentals } = fundamentalsReducer; 66 | return { fundamental: fundamentals[ownProps.symbol] }; 67 | } 68 | 69 | const mapDispatchToProps = (dispatch, ownProps) => ({ 70 | onFetchFundamental: () => { 71 | dispatch(askFundamental(ownProps.symbol)); 72 | } 73 | }) 74 | 75 | export default connect(mapStateToProps, mapDispatchToProps)(Statistics) 76 | -------------------------------------------------------------------------------- /src/actions/action_monitor.js: -------------------------------------------------------------------------------- 1 | const electron = window.require('electron'); 2 | const ipcRenderer = electron.ipcRenderer; 3 | ipcRenderer.on('asynchronous-reply', (event, arg) => { 4 | console.log(arg); 5 | }); 6 | ////////////MONITOR 7 | export const MONITOR_TICKER_ADD = 'MONITOR_TICKER_ADD' 8 | export const MONITOR_TICKER_DELETE = 'MONITOR_TICKER_DELETE' 9 | export const MONITOR_TICKER_PERCENTAGE_CHANGE = 'MONITOR_TICKER_PERCENTAGE_CHANGE' 10 | export const MONITOR_TICKER_LAST_PRICE_CHANGE = 'MONITOR_TICKER_LAST_PRICE_CHANGE' 11 | 12 | export const changeMonitorTickerLastPrice = (instrument_id, last_price) => ({ 13 | type: MONITOR_TICKER_LAST_PRICE_CHANGE, 14 | instrument_id, last_price 15 | }) 16 | 17 | export const changeMonitorTickerPercentage = (instrument_id, percentage) => ({ 18 | type: MONITOR_TICKER_PERCENTAGE_CHANGE, 19 | instrument_id, percentage 20 | }) 21 | 22 | export const deleteMonitorTicker = (instrument_id) => ({ 23 | type: MONITOR_TICKER_DELETE, 24 | instrument_id 25 | }) 26 | 27 | export const addMonitorTicker = (instrument_id, symbol, percentage, last_price) => ({ 28 | type: MONITOR_TICKER_ADD, 29 | instrument_id, symbol, percentage, last_price 30 | }) 31 | 32 | export const askMonitorTickers = () => (dispatch, getState) => { 33 | let symbolArray = Object.keys(getState().monitorReducer.tickers).map((instrument_id)=>{ 34 | return getState().monitorReducer.tickers[instrument_id].symbol; 35 | }); 36 | if(symbolArray.length === 0) { 37 | return; 38 | } 39 | 40 | return fetch(`https://api.robinhood.com/prices/?symbols=${symbolArray.join(',')}&source=nls`, { 41 | method: 'GET', 42 | headers: new Headers({ 43 | 'Accept': 'application/json', 44 | 'Authorization': getState().tokenReducer.token 45 | }) 46 | }) 47 | .then(response => response.json()) 48 | .then(jsonResult => { 49 | if(jsonResult.results) { 50 | jsonResult.results.forEach((result) => { 51 | let price = Number(result.price).toFixed(2); 52 | let difference = price - getState().monitorReducer.tickers[result.instrument_id].last_price; 53 | let differenceInpercentage = Math.abs(difference/getState().monitorReducer.tickers[result.instrument_id].last_price) * 100; 54 | 55 | if(differenceInpercentage >= getState().monitorReducer.tickers[result.instrument_id].percentage) { 56 | dispatch(changeMonitorTickerLastPrice(result.instrument_id, price)); 57 | ipcRenderer.send('price-alert', { 58 | title: "PRICE ALERT!!!", 59 | action: `robinhood://instrument/?id=${result.instrument_id}`, 60 | message: `$${getState().monitorReducer.tickers[result.instrument_id].symbol} is ${(difference > 0)? "up" : "down"} to ${differenceInpercentage.toFixed(2)}%. Current price is $${price}` 61 | }); 62 | } 63 | }) 64 | } 65 | else { 66 | console.log(jsonResult); 67 | } 68 | }) 69 | .catch(function(reason) { 70 | console.log(reason); 71 | }); 72 | } 73 | -------------------------------------------------------------------------------- /src/components/Position.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react' 2 | import '../styles/Position.css' 3 | 4 | const Position = ({ position, quote }) => { 5 | const quantity = Number(position.quantity); 6 | const average_buy_price = Number(position.average_buy_price); 7 | const intraday_average_buy_price = Number(position.intraday_average_buy_price); 8 | const intraday_quantity = Number(position.intraday_quantity); 9 | const previous_close = Number(quote.previous_close); 10 | 11 | const lastPrice = (quote.last_extended_hours_trade_price)? quote.last_extended_hours_trade_price : quote.last_trade_price; 12 | const equityValue = (lastPrice*quantity).toFixed(2); 13 | let totalReturn = (lastPrice - average_buy_price)*quantity; 14 | if(totalReturn < 0) { 15 | totalReturn = "-US$"+(-1)*totalReturn.toFixed(2) 16 | } 17 | else { 18 | totalReturn = "+US$"+totalReturn.toFixed(2) 19 | } 20 | const totalReturnPercentage = ( ( lastPrice - average_buy_price ) / average_buy_price*100 ).toFixed(2); 21 | //console.log(position); 22 | let todaysReturn = 0; 23 | let todaysReturnPercentage = 0; 24 | if( intraday_average_buy_price > 0 && intraday_quantity > 0 ) { 25 | let TodayOnlyReturn = (lastPrice - intraday_average_buy_price) * intraday_quantity; 26 | let notTodayQuantity = quantity - intraday_quantity; 27 | 28 | todaysReturn = (lastPrice - previous_close) * notTodayQuantity + TodayOnlyReturn; 29 | todaysReturnPercentage = ( (todaysReturn / ( previous_close * quantity )) * 100 ).toFixed(2); 30 | } 31 | else { 32 | todaysReturn = (lastPrice - previous_close)*quantity; 33 | todaysReturnPercentage = ( (lastPrice - previous_close) / previous_close * 100 ).toFixed(2); 34 | } 35 | 36 | if(todaysReturn < 0) { 37 | todaysReturn = "-US$"+(-1)*todaysReturn.toFixed(2) 38 | } 39 | else { 40 | todaysReturn = "+US$"+todaysReturn.toFixed(2) 41 | } 42 | 43 | return ( 44 |
    45 |
    46 |
    47 |
    {Number(quantity)}
    48 |
    Shares
    49 |
    50 |
    51 |
    ${equityValue}
    52 |
    Equity Value
    53 |
    54 |
    55 |
    56 |
    57 |
    Average Cost
    58 |
    ${Number(average_buy_price).toFixed(2)}
    59 |
    60 |
    61 |
    Total Return
    62 |
    {totalReturn} ({totalReturnPercentage}%)
    63 |
    64 |
    65 |
    Today's Return
    66 |
    {todaysReturn} ({todaysReturnPercentage}%)
    67 |
    68 |
    69 |
    70 | ) 71 | } 72 | 73 | 74 | Position.propTypes = { 75 | position: PropTypes.object.isRequired 76 | } 77 | 78 | export default Position 79 | -------------------------------------------------------------------------------- /src/components/Symbol.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; //, PropTypes 2 | import { DragSource, DropTarget } from 'react-dnd'; 3 | 4 | import {flow} from '../utils'; 5 | 6 | const style = { 7 | border: '1px dashed gray', 8 | padding: '5px 10px', 9 | margin: '2px', 10 | backgroundColor: 'white', 11 | color: 'black', 12 | cursor: 'move' 13 | }; 14 | 15 | class Symbol extends Component { 16 | 17 | render() { 18 | const { isDragging, connectDragSource, connectDropTarget, symbol } = this.props; 19 | const opacity = isDragging ? 0 : 1; 20 | 21 | return connectDragSource(connectDropTarget( 22 |
    (this.node = node)}> 23 | {symbol} 24 |
    25 | )); 26 | } 27 | } 28 | 29 | const cardSource = { 30 | 31 | beginDrag(props) { 32 | return { 33 | index: props.index, 34 | listId: props.listId, 35 | card: props.card, 36 | type: 'CARD' 37 | }; 38 | }, 39 | 40 | endDrag(props, monitor) { 41 | const item = monitor.getItem(); 42 | const dropResult = monitor.getDropResult(); 43 | 44 | if ( dropResult && dropResult.listId !== item.listId ) { 45 | props.removeCard(item.index); 46 | } 47 | } 48 | }; 49 | 50 | const cardTarget = { 51 | 52 | hover(props, monitor, component) { 53 | const dragIndex = monitor.getItem().index; 54 | const hoverIndex = props.index; 55 | const sourceListId = monitor.getItem().listId; 56 | 57 | // Don't replace items with themselves 58 | if (dragIndex === hoverIndex) { 59 | return; 60 | } 61 | 62 | // Determine rectangle on screen 63 | const hoverBoundingRect = component.node.getBoundingClientRect(); 64 | 65 | // Get vertical middle 66 | const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2; 67 | 68 | // Determine mouse position 69 | const clientOffset = monitor.getClientOffset(); 70 | 71 | // Get pixels to the top 72 | const hoverClientY = clientOffset.y - hoverBoundingRect.top; 73 | 74 | // Only perform the move when the mouse has crossed half of the items height 75 | // When dragging downwards, only move when the cursor is below 50% 76 | // When dragging upwards, only move when the cursor is above 50% 77 | 78 | // Dragging downwards 79 | if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) { 80 | return; 81 | } 82 | 83 | // Dragging upwards 84 | if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY) { 85 | return; 86 | } 87 | 88 | // Time to actually perform the action 89 | if ( props.listId === sourceListId ) { 90 | props.moveCard(dragIndex, hoverIndex); 91 | 92 | // Note: we're mutating the monitor item here! 93 | // Generally it's better to avoid mutations, 94 | // but it's good here for the sake of performance 95 | // to avoid expensive index searches. 96 | monitor.getItem().index = hoverIndex; 97 | } 98 | } 99 | }; 100 | 101 | export default flow([ 102 | DropTarget("CARD", cardTarget, connect => ({ 103 | connectDropTarget: connect.dropTarget() 104 | })), 105 | DragSource("CARD", cardSource, (connect, monitor) => ({ 106 | connectDragSource: connect.dragSource(), 107 | isDragging: monitor.isDragging() 108 | })) 109 | ])(Symbol); 110 | -------------------------------------------------------------------------------- /src/styles/PlaceOrder.css: -------------------------------------------------------------------------------- 1 | .tradeButtonWrapper { 2 | margin: 20px 0 5px 0; 3 | width: 100%; 4 | display: flex; 5 | justify-content:space-around; 6 | } 7 | 8 | .tradeButton { 9 | width: 45%; 10 | margin: 0 2.5%; 11 | padding: 10px 0; 12 | background-color: white; 13 | border-radius: 5px; 14 | color: black; 15 | text-align: center; 16 | border: none; 17 | font-weight: bold; 18 | cursor: pointer; 19 | position: relative; 20 | top: 0; 21 | flex: 1; 22 | } 23 | 24 | .tradeButton:hover { 25 | background-color: #E0F7F1; 26 | } 27 | 28 | .placeOrderWrapper { 29 | 30 | } 31 | 32 | .placeOrderHeader { 33 | background-color: var(--main-color); 34 | } 35 | 36 | .placeOrderHeader > h2 { 37 | font-size: 1.8rem; 38 | padding: 10px; 39 | color: white; 40 | } 41 | 42 | .placeOrderHeader > h3 { 43 | font-size: 1.4rem; 44 | padding: 6px 10px; 45 | font-weight: bold; 46 | background-color: #A9F5EB; 47 | color: black; 48 | } 49 | 50 | .placeOrderSection{ 51 | padding: 20px; 52 | } 53 | 54 | .orderOptionWrapper { 55 | width: 100%; 56 | display: flex; 57 | align-items:center; 58 | border-bottom: 1px solid var(--main-color); 59 | padding: 5px 0; 60 | margin-top: 10px; 61 | } 62 | 63 | .orderOptionName { 64 | width: 35%; 65 | margin-left: 5px; 66 | } 67 | 68 | .orderOption { 69 | width: 65%; 70 | } 71 | 72 | .orderWarning { 73 | margin-top: 3px; 74 | color: red; 75 | } 76 | 77 | .green { 78 | color: var(--main-color); 79 | margin-top: 0; 80 | } 81 | 82 | .grey { 83 | color: grey; 84 | margin-top: 0; 85 | border-bottom: 3px solid grey; 86 | } 87 | 88 | .resultWrapper { 89 | width: 100%; 90 | display: flex; 91 | padding: 5px 0; 92 | margin-top: 10px; 93 | font-weight: bold; 94 | font-size: 1.2rem; 95 | align-items:center; 96 | } 97 | 98 | .placeOrderButtonsWrapper { 99 | margin-top: 40px; 100 | display: flex; 101 | justify-content:space-between; 102 | } 103 | 104 | .placeOrderButton { 105 | background-color: white; 106 | color: black; 107 | border: 2px solid black; 108 | border-radius: 2px; 109 | padding: 5px; 110 | font-weight: bold; 111 | cursor: pointer; 112 | } 113 | 114 | .placeOrderButton:hover { 115 | background-color: #D8D8D8; 116 | } 117 | 118 | .cancel { 119 | background-color: #FA5882; 120 | color: white; 121 | } 122 | 123 | .cancel:hover { 124 | background-color: #F78181; 125 | } 126 | 127 | .orderingDiv { 128 | width: 100%; 129 | color: black; 130 | text-align: center; 131 | margin-top: calc(50%); 132 | padding: 10px 0; 133 | background-color: var(--main-color); 134 | } 135 | 136 | .orderedDivWrapper { 137 | text-align: center; 138 | } 139 | 140 | .orderedDiv { 141 | width: 100%; 142 | color: black; 143 | text-align: center; 144 | padding: 10px 0; 145 | color: white; 146 | background-color: var(--main-color); 147 | } 148 | 149 | .orderedReasonDiv { 150 | width: 100%; 151 | color: black; 152 | text-align: center; 153 | padding: 15px 0; 154 | } 155 | 156 | .orderedButton { 157 | background-color: white; 158 | color: black; 159 | border: 2px solid black; 160 | border-radius: 2px; 161 | padding: 5px; 162 | font-weight: bold; 163 | cursor: pointer; 164 | margin-top: 20px; 165 | } 166 | 167 | .orderedButton:hover { 168 | background-color: #D8D8D8; 169 | } 170 | -------------------------------------------------------------------------------- /src/reducers/reducer_positions.js: -------------------------------------------------------------------------------- 1 | import * as actions from '../actions'; 2 | 3 | const positionsReducer = (state = { 4 | isAskingPositions: false, 5 | error: "", 6 | positions: [], 7 | positionsWithZero: [], 8 | eachPosition: {} 9 | }, action) => { 10 | switch (action.type) { 11 | case actions.ASKING_POSITIONS: 12 | return { 13 | ...state, 14 | error: "", 15 | isAskingPositions: true 16 | } 17 | case actions.ASKING_POSITIONS_FAILED: 18 | return { 19 | ...state, 20 | isAskingPositions: false, 21 | error: action.error, 22 | positions: [] 23 | } 24 | case actions.ADD_POSITIONS: 25 | return { 26 | ...state, 27 | isAskingPositions: false, 28 | positions: action.positions, 29 | } 30 | case actions.ADD_MORE_POSITIONS: 31 | return { 32 | ...state, 33 | isAskingPositions: false, 34 | positions: state.positions.concat(action.positions) 35 | } 36 | case actions.DELETE_POSITIONS: 37 | return { 38 | isAskingPositions: false, 39 | error: "", 40 | positions: [], 41 | positionsWithZero: [], 42 | eachPosition: {} 43 | } 44 | case actions.ADD_POSITION: 45 | let tempPosition = {}; 46 | tempPosition[action.position.instrument] = action.position; 47 | return { 48 | ...state, 49 | eachPosition: Object.assign({}, state.eachPosition, tempPosition) 50 | } 51 | case actions.ADD_POSITIONS_WITH_ZERO: 52 | return { 53 | ...state, 54 | positionsWithZero: action.positions, 55 | } 56 | case actions.ADD_MORE_POSITIONS_WITH_ZERO: 57 | return { 58 | ...state, 59 | positionsWithZero: state.positionsWithZero.concat(action.positions) 60 | } 61 | default: 62 | return state 63 | } 64 | } 65 | 66 | export default positionsReducer 67 | 68 | 69 | /* 70 | "positions":[ 71 | { 72 | "account":"https://api.robinhood.com/accounts/accountnumber/", 73 | "intraday_quantity":"0.0000", 74 | "intraday_average_buy_price":"0.0000", 75 | "url":"https://api.robinhood.com/positions/accountnumber/stringOfSomething/", 76 | "created_at":"2017-03-02T03:52:54.895476Z", 77 | "updated_at":"2017-03-28T16:23:05.714421Z", 78 | "shares_held_for_buys":"0.0000", 79 | "average_buy_price":"1.33", 80 | "instrument":"https://api.robinhood.com/instruments/stringOfSomething/", 81 | "shares_held_for_sells":"0.0000", 82 | "quantity":"495.0000" 83 | }, 84 | ] 85 | 86 | "eachPosition":{ 87 | "https://api.robinhood.com/instruments/stringOfSomething/":{ 88 | "account":"https://api.robinhood.com/accounts/accountnumber/", 89 | "intraday_quantity":"0.0000", 90 | "intraday_average_buy_price":"0.0000", 91 | "url":"https://api.robinhood.com/positions/stringOfSomething/", 92 | "created_at":"2017-03-02T03:52:54.895476Z", 93 | "updated_at":"2017-03-28T16:23:05.714421Z", 94 | "shares_held_for_buys":"0.0000", 95 | "average_buy_price":"6.33", 96 | "instrument":"https://api.robinhood.com/instruments/stringOfSomething/", 97 | "shares_held_for_sells":"0.0000", 98 | "quantity":"100.0000" 99 | }, 100 | } 101 | */ 102 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | export const myFixed = (number) => { 2 | if(Number(number) < 1){ 3 | return Number( number.toFixed(4) ).toString(); 4 | } 5 | else { 6 | return Number(number).toFixed(2); 7 | } 8 | } 9 | 10 | export const capFirst = (string) => { 11 | if(typeof string !== 'string'){ 12 | return `${string}`; 13 | } 14 | return string.charAt(0).toUpperCase() + string.slice(1).toLowerCase(); 15 | } 16 | 17 | export const formatAMPM = (dateString) => { 18 | let date = new Date(dateString); 19 | var hours = date.getHours(); 20 | var minutes = date.getMinutes(); 21 | var ampm = hours >= 12 ? 'pm' : 'am'; 22 | hours %= 12; 23 | hours = hours ? hours : 12; // the hour '0' should be '12' 24 | hours = (hours<10)? '0'+hours : hours; 25 | minutes = minutes < 10 ? '0'+minutes : minutes; 26 | var strTime = hours + ':' + minutes + ' ' + ampm; 27 | 28 | return strTime; 29 | } 30 | 31 | export const printDate = (dateString) => { 32 | let newsDate = new Date(dateString); 33 | let now = new Date(); 34 | let res = ""; 35 | if(newsDate.getDate()===now.getDate() && 36 | newsDate.getMonth()===now.getMonth() && 37 | newsDate.getFullYear()===now.getFullYear() 38 | ){ 39 | res = formatAMPM(dateString); 40 | } 41 | else{ 42 | res = `${newsDate.getMonth()+1}/${newsDate.getDate()}/${newsDate.getFullYear()}` 43 | } 44 | 45 | return res; 46 | } 47 | 48 | export const printDateOnly = (dateString) => { 49 | let newsDate = new Date(dateString); 50 | 51 | return `${newsDate.getMonth()+1}/${newsDate.getDate()}/${newsDate.getFullYear()}`; 52 | } 53 | 54 | 55 | // a and b are string 56 | export const dateDiffInDays = (a) => { 57 | const _MS_PER_DAY = 1000 * 60 * 60 * 24; 58 | let aDate = new Date(a); 59 | let bDate = new Date(); 60 | // Discard the time and time-zone information. 61 | var utc1 = Date.UTC(aDate.getFullYear(), aDate.getMonth(), aDate.getDate()); 62 | var utc2 = Date.UTC(bDate.getFullYear(), bDate.getMonth(), bDate.getDate()); 63 | 64 | return Math.floor((utc2 - utc1) / _MS_PER_DAY); 65 | } 66 | 67 | export const displayPercentage = (newString, oldString) => { 68 | let newNum = Number(newString); 69 | let oldNum = Number(oldString); 70 | let negPos = (newNum - oldNum > 0)? "+" : ""; 71 | 72 | return negPos + ((newNum - oldNum) / oldNum * 100).toFixed(2) + "%" 73 | } 74 | 75 | export const isLater = (newDateStr, oldDateStr) => { 76 | return new Date(newDateStr) > new Date(oldDateStr); 77 | } 78 | 79 | export const flow = (funcs) => { 80 | const length = funcs ? funcs.length : 0 81 | let index = length 82 | while (index--) { 83 | if (typeof funcs[index] !== 'function') { 84 | throw new TypeError('Expected a function') 85 | } 86 | } 87 | return function(...args) { 88 | let index = 0 89 | let result = length ? funcs[index].apply(this, args) : args[0] 90 | while (++index < length) { 91 | result = funcs[index].call(this, result) 92 | } 93 | return result 94 | } 95 | } 96 | 97 | export const carry = (numString) => { 98 | const numNum = Number(numString); 99 | let res = ""; 100 | if(numNum > 1000000000){ 101 | res = Math.round(numNum/1000000000 * 100) / 100 + "B" 102 | } 103 | else if(numNum > 1000000){ 104 | res = Math.round(numNum/1000000 * 1000) / 1000 + "M" 105 | } 106 | else { 107 | res = Number(numNum); 108 | } 109 | 110 | return res; 111 | } 112 | -------------------------------------------------------------------------------- /src/containers/PendingOrdersPage.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | import { connect } from 'react-redux' 3 | import { askCurrentOrder, cancelOrder } from '../actions' 4 | import SectionWrapper from '../components/SectionWrapper' 5 | import Orders from '../components/Orders' 6 | import { askHistoricalsOrders } from '../actions' 7 | import '../styles/HistoryPage.css' 8 | 9 | class PendingOrdersPage extends Component { 10 | static propTypes = { 11 | isAskingCurrentOrder: PropTypes.bool.isRequired, 12 | currentOrder: PropTypes.object.isRequired, 13 | currentOrderFailedReason: PropTypes.string.isRequired, 14 | cancelCurrentOrderState: PropTypes.string.isRequired, 15 | instruments: PropTypes.object.isRequired, 16 | isCurrent: PropTypes.bool.isRequired 17 | } 18 | 19 | componentDidMount() { 20 | this.props.onFetchHistoricalsOrders(); 21 | } 22 | 23 | askCurrentOrder = (orderId) => { 24 | this.props.onFetchCurrentOrder(orderId); 25 | } 26 | 27 | cancelOrder = (cancelLink, orderId) => { 28 | this.props.onCancelOrder(cancelLink, orderId); 29 | } 30 | 31 | render() { 32 | const props = { ...this.props }; 33 | 34 | //show null if not cuttent page 35 | if(!this.props.isCurrent){ return null; } 36 | 37 | const pendingOrdersArray = Object.keys(props.pendingOrders).map( orderID => props.pendingOrders[orderID] ); 38 | //sort by date 39 | pendingOrdersArray.sort((a, b) => { 40 | a = new Date(a.created_at); 41 | b = new Date(b.created_at); 42 | return a>b ? -1 : a console.log("this is for pending orders") } 51 | askCurrentOrder={ this.askCurrentOrder } 52 | cancelOrder={ this.cancelOrder } 53 | /> 54 | : "Loading..."; 55 | 56 | return ( 57 |
    58 |
    59 |
    60 |
    61 |

    Pending Orders

    62 |
    63 |
    64 | 65 | 66 | { pendingOrdersBlock } 67 | 68 | 69 |
    70 | ) 71 | } 72 | } 73 | 74 | const mapStateToProps = ({ ordersReducer, instrumentsReducer }) => { 75 | const { 76 | pendingOrders, 77 | isAskingCurrentOrder, 78 | currentOrder, 79 | currentOrderFailedReason, 80 | cancelCurrentOrderState 81 | } = ordersReducer; 82 | 83 | const { instruments } = instrumentsReducer; 84 | 85 | return { 86 | pendingOrders, 87 | isAskingCurrentOrder, 88 | currentOrder, 89 | currentOrderFailedReason, 90 | cancelCurrentOrderState, 91 | instruments 92 | }; 93 | } 94 | 95 | const mapDispatchToProps = (dispatch, ownProps) => ({ 96 | onFetchHistoricalsOrders: () => { 97 | dispatch(askHistoricalsOrders()); 98 | }, 99 | onFetchCurrentOrder: (orderId) => { 100 | dispatch(askCurrentOrder(orderId)) 101 | }, 102 | onCancelOrder: (cancelLink, orderId) => { 103 | dispatch(cancelOrder(cancelLink, orderId)); 104 | } 105 | }) 106 | 107 | export default connect(mapStateToProps, mapDispatchToProps)(PendingOrdersPage) 108 | -------------------------------------------------------------------------------- /src/containers/Earnings.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | import { connect } from 'react-redux' 3 | 4 | import SectionWrapper from '../components/SectionWrapper' 5 | import EarningsChart from '../components/EarningsChart' 6 | import { askEarnings } from '../actions' 7 | import '../styles/Earnings.css' 8 | 9 | class Earnings extends Component { 10 | static propTypes = { 11 | symbol: PropTypes.string.isRequired 12 | } 13 | 14 | 15 | componentDidMount() { 16 | this.props.onFetchEarnings(); 17 | } 18 | 19 | render() { 20 | const earnings = this.props.earnings; 21 | 22 | if(!earnings || earnings.length === 0) return null; 23 | 24 | let localLast = (earnings.length > 2)? earnings.length-2 : earnings.length-1; 25 | 26 | let displayData = []; 27 | for(let i = localLast; i > 0; i--) { 28 | if( i === earnings.length-7 ){ break; } 29 | let tempEstimate = {} 30 | let tempActual = {} 31 | 32 | tempEstimate.xIndex = 5 - (localLast - i) ; 33 | tempEstimate.year = earnings[i].year; 34 | tempEstimate.quarter = `Q${earnings[i].quarter}`; 35 | tempEstimate.eps = ( earnings[i].eps.estimate )? Math.round(Number(earnings[i].eps.estimate) * 100) / 100 : null; 36 | tempEstimate.epsType = "estimate"; 37 | 38 | tempActual.xIndex = 5 - (localLast - i) ; 39 | tempActual.year = earnings[i].year; 40 | tempActual.quarter = `Q${earnings[i].quarter}`; 41 | tempActual.eps = ( earnings[i].eps.actual )? Math.round(Number(earnings[i].eps.actual) * 100) / 100 : null; 42 | tempActual.epsType = "actual"; 43 | 44 | displayData.push(tempEstimate); 45 | displayData.push(tempActual); 46 | } 47 | 48 | return ( 49 | 50 | 51 | 52 |
    53 |
    54 |
    Expected EPS •
    55 |
    56 | {(earnings[localLast].eps.estimate)? 57 | `${Number(earnings[localLast].eps.estimate) < 0 ? "-" : ""}US$${Math.abs(Number(earnings[localLast].eps.estimate)).toFixed(2)}` 58 | : "N/A"} 59 |
    60 |
    61 |
    62 |
    Actual EPS •
    63 |
    64 | {(earnings[localLast].eps.actual)? 65 | `${Number(earnings[localLast].eps.actual) < 0 ? "-" : ""}US$${Math.abs(Number(earnings[localLast].eps.actual)).toFixed(2)}` 66 | : (earnings[localLast].report)? ( 67 |
    68 |
    69 | {`${(earnings[localLast].report.verified)? "Available" : "Expected"} on ${earnings[localLast].report.date},`} 70 |
    71 |
    72 | {(earnings[localLast].report.timing === "am")? "Pre-market" : "After-hours"} 73 |
    74 |
    75 | ) : null } 76 |
    77 |
    78 |
    79 |
    80 | ) 81 | } 82 | } 83 | 84 | const mapStateToProps = ({ earningsReducer }, ownProps) => { 85 | const { earningsAll } = earningsReducer; 86 | return { earnings: earningsAll[ownProps.symbol] }; 87 | } 88 | 89 | const mapDispatchToProps = (dispatch, ownProps) => ({ 90 | onFetchEarnings: () => { 91 | dispatch(askEarnings(ownProps.symbol)); 92 | } 93 | }) 94 | 95 | export default connect(mapStateToProps, mapDispatchToProps)(Earnings) 96 | -------------------------------------------------------------------------------- /src/actions/action_login.js: -------------------------------------------------------------------------------- 1 | ////////////LOGIN 2 | ////////////ACCOUNT 3 | export const ACCOUNT_RESET_ERROR = 'ACCOUNT_RESET_ERROR' 4 | export const ACCOUNT_ADD = 'ACCOUNT_ADD' 5 | export const ACCOUNT_DELETE = 'ACCOUNT_DELETE' 6 | export const ACCOUNT_ASKING = 'ACCOUNT_ASKING' 7 | export const ACCOUNT_ASKING_FAILED = 'ACCOUNT_ASKING_FAILED' 8 | ////////////TOKEN 9 | export const TOKEN_RESET_ERROR = 'TOKEN_RESET_ERROR' 10 | export const TOKEN_ADD = 'TOKEN_ADD' 11 | export const TOKEN_DELETE = 'TOKEN_DELETE' 12 | export const TOKEN_ASKING = 'TOKEN_ASKING' 13 | export const TOKEN_ASKING_FAILED = 'TOKEN_ASKING_FAILED' 14 | export const TOKEN_NEED_MFA = 'TOKEN_NEED_MFA' 15 | 16 | export const resetAccountError = () => ({ 17 | type: ACCOUNT_RESET_ERROR 18 | }) 19 | 20 | export const askingAccountFailed = (error) => ({ 21 | type: ACCOUNT_ASKING_FAILED, 22 | error 23 | }) 24 | 25 | export const askingAccount = () => ({ 26 | type: ACCOUNT_ASKING 27 | }) 28 | 29 | export const addAccount = account => ({ 30 | type: ACCOUNT_ADD, 31 | account 32 | }) 33 | 34 | export const deleteAccount = () => ({ 35 | type: ACCOUNT_DELETE 36 | }) 37 | 38 | export const askAccount = () => (dispatch, getState) => { 39 | dispatch(askingAccount()); 40 | return fetch(`https://api.robinhood.com/accounts/`, { 41 | method: 'GET', 42 | headers: new Headers({ 43 | 'Accept': 'application/json', 44 | 'Authorization': getState().tokenReducer.token 45 | }) 46 | }) 47 | .then(response => response.json()) 48 | .then(jsonResult => { 49 | if(jsonResult.results){ 50 | dispatch(addAccount(jsonResult.results[0])); 51 | } 52 | else { 53 | //ERROR: {"detail":"Authentication credentials were not provided."} 54 | dispatch(askingAccountFailed(jsonResult[Object.keys(jsonResult)[0]][0])); 55 | } 56 | }) 57 | .catch(function(reason) { 58 | console.log(reason); 59 | dispatch(askingAccountFailed(reason)); 60 | }); 61 | } 62 | 63 | export const resetTokenError = () => ({ 64 | type: TOKEN_RESET_ERROR 65 | }) 66 | 67 | export const askingTokenFailed = (error) => ({ 68 | type: TOKEN_ASKING_FAILED, 69 | error 70 | }) 71 | 72 | export const askingToken = () => ({ 73 | type: TOKEN_ASKING 74 | }) 75 | 76 | export const addToken = token => ({ 77 | type: TOKEN_ADD, 78 | token 79 | }) 80 | 81 | export const deleteToken = () => ({ 82 | type: TOKEN_DELETE 83 | }) 84 | 85 | export const needMFA = () => ({ 86 | type: TOKEN_NEED_MFA 87 | }) 88 | 89 | export const askToken = (username, password, mfa) => (dispatch, getState) => { 90 | dispatch(askingToken()); 91 | const bodyString = (getState().tokenReducer.needMFA)? 92 | JSON.stringify({ username: username, password: password, mfa_code: mfa }) : 93 | JSON.stringify({ username: username, password: password }); 94 | 95 | return fetch(`https://api.robinhood.com/api-token-auth/`, { 96 | method: 'POST', 97 | headers: new Headers({'content-type': 'application/json', 'Accept': 'application/json'}), 98 | body: bodyString 99 | }) 100 | .then(response => response.json()) 101 | .then(jsonResult => { 102 | if(jsonResult.hasOwnProperty("token")){ 103 | dispatch(addToken(jsonResult.token)); 104 | dispatch(askAccount()); 105 | } 106 | else if(jsonResult.mfa_required) { 107 | dispatch(needMFA()) 108 | } 109 | else { 110 | dispatch(askingTokenFailed(jsonResult[Object.keys(jsonResult)[0]][0])); 111 | } 112 | }) 113 | .catch(function(reason) { 114 | console.log(reason); 115 | dispatch(askingTokenFailed(reason)); 116 | }); 117 | } 118 | -------------------------------------------------------------------------------- /src/components/QuotesForPortfolios.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | import { LineChart, Line, XAxis, YAxis, Tooltip, ReferenceLine, ResponsiveContainer } from 'recharts'; 3 | import WithoutTimeTooltip from './WithoutTimeTooltip' 4 | import WithTimeTooltip from './WithTimeTooltip' 5 | import { dateDiffInDays } from '../utils' 6 | import '../styles/Quotes.css' 7 | 8 | class QuotesForPortfolios extends Component { 9 | static propTypes = { 10 | historicals: PropTypes.array.isRequired, 11 | previous_close: PropTypes.number.isRequired, 12 | selectedButtonName: PropTypes.string.isRequired 13 | } 14 | 15 | constructor(props) { 16 | super(props); 17 | this.state = {width: 0}; 18 | } 19 | 20 | componentDidMount() { 21 | window.addEventListener('resize', this.resetDimensions); 22 | this.resetDimensions(); 23 | } 24 | 25 | componentWillUnmount() { 26 | window.removeEventListener('resize', this.resetDimensions); 27 | } 28 | 29 | resetDimensions = () => { 30 | this.setState({width: this.qw.offsetWidth }); 31 | this.qw.style.height = this.qw.offsetWidth>500? "250px" : this.qw.offsetWidth/2+"px"; 32 | } 33 | 34 | render() { 35 | const { selectedButtonName, previous_close, historicals, last_equity } = this.props; 36 | let data = []; 37 | if(selectedButtonName === "1M"){ 38 | historicals.forEach((eachData)=>{ 39 | if(dateDiffInDays(eachData.begins_at) <= 31){ 40 | data.push(eachData); 41 | } 42 | }) 43 | } 44 | else if(selectedButtonName === "3M"){ 45 | historicals.forEach((eachData)=>{ 46 | if(dateDiffInDays(eachData.begins_at) <= 92){ 47 | data.push(eachData); 48 | } 49 | }) 50 | } 51 | else { 52 | data = historicals; 53 | } 54 | 55 | const strokeColor = (selectedButtonName==="1D")? ( 56 | (previous_close < last_equity)? '#00FF73' : '#F35A2B' 57 | ):(data[0].not_reg_close_equity < last_equity)? '#00FF73' : '#F35A2B'; 58 | /* 59 | const strokeColor = (selectedButtonName==="1D")? ( 60 | (previous_close < data[data.length-1].adjusted_close_equity)? '#00FF73' : '#F35A2B' 61 | ):(data[0].not_reg_close_equity < data[data.length-1].not_reg_close_equity)? '#00FF73' : '#F35A2B'; 62 | */ 63 | 64 | return ( 65 |
    { this.qw = div; }} > 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | {selectedButtonName==="1D" || selectedButtonName==="1W"? 74 | }/>: 75 | }/> 76 | } 77 | {(selectedButtonName==="1D")? ( 78 | 79 | ):null} 80 | 81 | 82 | 83 |
    84 | ) 85 | } 86 | } 87 | 88 | export default QuotesForPortfolios 89 | -------------------------------------------------------------------------------- /src/containers/HistoryPage.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | import { connect } from 'react-redux' 3 | import { 4 | askHistoricalsOrders, askCurrentOrder, cancelOrder 5 | } from '../actions' 6 | import SectionWrapper from '../components/SectionWrapper' 7 | import Orders from '../components/Orders' 8 | import '../styles/HistoryPage.css' 9 | 10 | class HistoryPage extends Component { 11 | static propTypes = { 12 | historicalsOrders: PropTypes.array.isRequired, 13 | historicalsOrdersNextLink: PropTypes.string.isRequired, 14 | isAskingCurrentOrder: PropTypes.bool.isRequired, 15 | currentOrder: PropTypes.object.isRequired, 16 | currentOrderFailedReason: PropTypes.string.isRequired, 17 | cancelCurrentOrderState: PropTypes.string.isRequired, 18 | instruments: PropTypes.object.isRequired, 19 | isCurrent: PropTypes.bool.isRequired 20 | } 21 | 22 | componentDidMount(){ 23 | this.props.onFetchHistoricalsOrders(); 24 | } 25 | 26 | componentWillReceiveProps(nextProps){ 27 | if(nextProps.isCurrent && !this.props.isCurrent){ 28 | this.props.onFetchHistoricalsOrders(); 29 | } 30 | } 31 | 32 | addMoreHistoricalsOrder = () => { 33 | this.props.onFetchHistoricalsOrders(this.props.historicalsOrdersNextLink); 34 | } 35 | 36 | askCurrentOrder = (orderId) => { 37 | this.props.onFetchCurrentOrder(orderId); 38 | } 39 | 40 | cancelOrder = (cancelLink, orderId) => { 41 | this.props.onCancelOrder(cancelLink, orderId); 42 | } 43 | 44 | render() { 45 | const props = { ...this.props }; 46 | 47 | //show null if not cuttent page 48 | if(!this.props.isCurrent){ return null; } 49 | 50 | const historicalsOrdersBlock = ( props.historicalsOrders )? 51 | 57 | : "Loading..."; 58 | 59 | return ( 60 |
    61 |
    62 |
    63 |
    64 |

    History

    65 |
    66 |
    67 | 68 | 69 | { historicalsOrdersBlock } 70 | 71 | 72 | 73 |
    74 | ) 75 | } 76 | } 77 | 78 | const mapStateToProps = ({ ordersReducer, instrumentsReducer }) => { 79 | const { 80 | historicalsOrders, 81 | historicalsOrdersNextLink, 82 | isAskingCurrentOrder, 83 | currentOrder, 84 | currentOrderFailedReason, 85 | cancelCurrentOrderState 86 | } = ordersReducer; 87 | 88 | const { instruments } = instrumentsReducer; 89 | 90 | return { 91 | historicalsOrders, 92 | historicalsOrdersNextLink, 93 | isAskingCurrentOrder, 94 | currentOrder, 95 | currentOrderFailedReason, 96 | cancelCurrentOrderState, 97 | instruments 98 | }; 99 | } 100 | 101 | const mapDispatchToProps = (dispatch, ownProps) => ({ 102 | onFetchHistoricalsOrders: (link) => { 103 | if(!link) { 104 | dispatch(askHistoricalsOrders()); 105 | } 106 | else{ 107 | dispatch(askHistoricalsOrders(link)); 108 | } 109 | }, 110 | onFetchCurrentOrder: (orderId) => { 111 | dispatch(askCurrentOrder(orderId)) 112 | }, 113 | onCancelOrder: (cancelLink, orderId) => { 114 | dispatch(cancelOrder(cancelLink, orderId)); 115 | } 116 | }) 117 | 118 | export default connect(mapStateToProps, mapDispatchToProps)(HistoryPage) 119 | -------------------------------------------------------------------------------- /src/containers/LoginPage.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | import { connect } from 'react-redux' 3 | import { askToken, resetTokenError } from '../actions' 4 | import Input from '../components/Input' 5 | import '../styles/LoginPage.css' 6 | 7 | class LoginPage extends Component { 8 | static propTypes = { 9 | tokenError: PropTypes.string.isRequired, 10 | isAskingToken: PropTypes.bool.isRequired, 11 | accountError: PropTypes.string.isRequired, 12 | isAskingAccount: PropTypes.bool.isRequired, 13 | needMFA: PropTypes.bool.isRequired 14 | } 15 | 16 | constructor(props) { 17 | super(props); 18 | this.state = { 19 | username: "", 20 | password: "", 21 | mfa: "", 22 | unMessage: "", 23 | pwMessage: "", 24 | mfaMessage:"" 25 | }; 26 | } 27 | 28 | componentDidMount() { 29 | this.props.onResetTokenError(); 30 | } 31 | 32 | handleUsernameChange = event => { 33 | this.setState({username: event.target.value, unMessage:""}); 34 | } 35 | 36 | handlePasswordChange = event => { 37 | this.setState({password: event.target.value, pwMessage:""}); 38 | } 39 | 40 | handleMFAChange = event => { 41 | this.setState({mfa: event.target.value, mfaMessage:""}); 42 | } 43 | 44 | handleSubmit = event => { 45 | event.preventDefault(); 46 | if((this.state.username === "") || (this.state.password === "")){ 47 | if(this.state.username === "") { this.setState({unMessage: "This field may not be blank."}); } 48 | if(this.state.password === "") { this.setState({pwMessage: "This field may not be blank."}); } 49 | return; 50 | } 51 | if(this.props.needMFA && this.state.mfa === ""){ 52 | this.setState({mfaMessage: "This field may not be blank."}); 53 | return; 54 | } 55 | 56 | this.props.onFetchToken(this.state.username, this.state.password, this.state.mfa) 57 | } 58 | 59 | render() { 60 | const { isAskingToken, tokenError, needMFA, isAskingAccount, accountError } = this.props 61 | const isAsking = isAskingToken || isAskingAccount; 62 | const Errors = tokenError || accountError; 63 | 64 | return ( 65 |
    66 |
    67 |

    ROBINDAHOOD

    68 |
    {Errors}
    69 |
    70 | 71 | 72 | {(needMFA)? ( 73 | 74 | ) : null} 75 | 76 |
    77 |
    78 | ) 79 | } 80 | } 81 | 82 | const mapStateToProps = ({ tokenReducer, accountReducer }) => { 83 | const { 84 | isAskingToken, 85 | tokenError, 86 | needMFA 87 | } = tokenReducer; 88 | const { 89 | isAskingAccount, 90 | accountError 91 | } = accountReducer; 92 | 93 | return { 94 | isAskingToken, 95 | tokenError, 96 | needMFA, 97 | isAskingAccount, 98 | accountError 99 | }; 100 | } 101 | 102 | const mapDispatchToProps = (dispatch, ownProps) => ({ 103 | onFetchToken: (username, password, mfa) => { 104 | dispatch(askToken(username, password, mfa)); 105 | }, 106 | onResetTokenError: () => { 107 | dispatch(resetTokenError()); 108 | } 109 | }) 110 | 111 | export default connect(mapStateToProps, mapDispatchToProps)(LoginPage) 112 | -------------------------------------------------------------------------------- /src/components/Quotes.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | import { LineChart, Line, XAxis, YAxis, Tooltip, ReferenceLine, ResponsiveContainer } from 'recharts'; 3 | import WithoutTimeTooltip from './WithoutTimeTooltip' 4 | import WithTimeTooltip from './WithTimeTooltip' 5 | import { dateDiffInDays } from '../utils' 6 | import '../styles/Quotes.css' 7 | 8 | class Quotes extends Component { 9 | static propTypes = { 10 | historicals: PropTypes.array.isRequired, 11 | previous_close: PropTypes.string.isRequired, 12 | selectedButtonName: PropTypes.string.isRequired 13 | } 14 | 15 | constructor(props) { 16 | super(props); 17 | this.state = {width: 0}; 18 | } 19 | 20 | componentDidMount() { 21 | window.addEventListener('resize', this.resetDimensions); 22 | this.resetDimensions(); 23 | } 24 | 25 | componentWillUnmount() { 26 | window.removeEventListener('resize', this.resetDimensions); 27 | } 28 | 29 | shouldComponentUpdate(nextProps, nextState) { 30 | if(nextProps.selectedButtonName !== "1D" && this.props.selectedButtonName === nextProps.selectedButtonName) { 31 | return false; 32 | } 33 | 34 | return true; 35 | } 36 | 37 | resetDimensions = () => { 38 | this.setState({width: this.qw.offsetWidth }); 39 | this.qw.style.height = ( this.qw.offsetWidth > 500 )? "250px" : this.qw.offsetWidth/2+"px"; 40 | } 41 | 42 | render() { 43 | const { selectedButtonName, historicals, previous_close, last_price } = this.props; 44 | let data = []; 45 | if(selectedButtonName === "1M"){ 46 | historicals.forEach((eachData)=>{ 47 | if(dateDiffInDays(eachData.begins_at) <= 31){ 48 | data.push(eachData); 49 | } 50 | }) 51 | } 52 | else if(selectedButtonName === "3M"){ 53 | historicals.forEach((eachData)=>{ 54 | if(dateDiffInDays(eachData.begins_at) <= 92){ 55 | data.push(eachData); 56 | } 57 | }) 58 | } 59 | else { 60 | data = historicals; 61 | } 62 | 63 | ///* 64 | const strokeColor = (selectedButtonName==="1D")? ( 65 | (previous_close < last_price)? '#00FF73' : '#F35A2B' 66 | ):(data[0].close_price < last_price)? '#00FF73' : '#F35A2B'; 67 | //*/ 68 | /* 69 | const strokeColor = (selectedButtonName==="1D")? ( 70 | (previous_close < data[data.length-1].close_price)? '#00FF73' : '#F35A2B' 71 | ):(data[0].close_price < data[data.length-1].close_price)? '#00FF73' : '#F35A2B'; 72 | */ 73 | 74 | return ( 75 |
    { this.qw = div; }} > 76 | 77 | 78 | 79 | 80 | 81 | 82 | data.toFixed(2)}/> 83 | {selectedButtonName==="1D" || selectedButtonName==="1W"? 84 | }/>: 85 | }/> 86 | } 87 | {(selectedButtonName==="1D")? ( 88 | 89 | ):null} 90 | 91 | 92 | 93 |
    94 | ) 95 | } 96 | } 97 | 98 | export default Quotes 99 | -------------------------------------------------------------------------------- /src/components/EarningsChart.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | import { ResponsiveContainer, ScatterChart, Scatter, XAxis, YAxis, Tooltip, Cell } from 'recharts' 3 | import '../styles/Earnings.css' 4 | 5 | class CustomizedXLabel extends Component { 6 | render() { 7 | let { x, y, displayData, payload } = this.props; 8 | let index = -1; 9 | for(let i=0; i 20 | 21 | {displayData[index].quarter} 22 | {displayData[index].year} 23 | 24 | 25 | ); 26 | } 27 | } 28 | 29 | class CustomizedYLabel extends Component { 30 | render() { 31 | let { x, y, payload } = this.props; 32 | 33 | return ( 34 | 35 | 36 | {payload.value} 37 | 38 | 39 | ); 40 | } 41 | } 42 | 43 | class EarningsTooltip extends Component { 44 | render() { 45 | const { active } = this.props; 46 | 47 | if (active) { 48 | const { payload } = this.props; 49 | const eps = payload[0].payload; 50 | 51 | return ( 52 |
    53 |
    {`${eps.quarter} ${eps.year}`}
    54 |
    {`${eps.epsType.toUpperCase()} EPS`}
    55 |
    {eps.eps}
    56 |
    57 | ); 58 | } 59 | 60 | return null; 61 | } 62 | } 63 | 64 | class EarningsChart extends Component { 65 | static propTypes = { 66 | data: PropTypes.array.isRequired 67 | } 68 | 69 | constructor(props) { 70 | super(props); 71 | this.state = {width: 0}; 72 | } 73 | 74 | componentDidMount() { 75 | window.addEventListener('resize', this.resetDimensions); 76 | this.resetDimensions(); 77 | } 78 | 79 | componentWillUnmount() { 80 | window.removeEventListener('resize', this.resetDimensions); 81 | } 82 | 83 | resetDimensions = () => { 84 | this.setState({width: this.ew.offsetWidth }); 85 | //console.log(this.state.width); 86 | this.ew.style.height = ( this.ew.offsetWidth > 500 )? "250px" : this.ew.offsetWidth/2+"px"; 87 | } 88 | 89 | render() { 90 | const { data } = this.props; 91 | 92 | return ( 93 |
    { this.ew = div; }} > 94 | 95 | 96 | } /> 97 | } /> 98 | 99 | { 100 | data.map((entry, index) => ( 101 | 102 | )) 103 | } 104 | 105 | } /> 106 | 107 | 108 |
    109 | ) 110 | } 111 | } 112 | 113 | export default EarningsChart 114 | -------------------------------------------------------------------------------- /src/components/Search.js: -------------------------------------------------------------------------------- 1 | import React, { Component,PropTypes } from 'react' 2 | import Autosuggest from 'react-autosuggest'; 3 | import '../styles/Search.css' 4 | /* ----------- */ 5 | /* Utils */ 6 | /* ----------- */ 7 | 8 | // https://developer.mozilla.org/en/docs/Web/JavaScript/Guide/Regular_Expressions#Using_Special_Characters 9 | function escapeRegexCharacters(str) { 10 | return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); 11 | } 12 | 13 | /* --------------- */ 14 | /* Component */ 15 | /* --------------- */ 16 | 17 | const getSuggestionValue = (suggestion) => (suggestion.symbol); 18 | 19 | const renderSuggestionsContainer = ({ containerProps , children, query }) => { 20 | return ( 21 |
    22 | {children} 23 |
    24 | ); 25 | } 26 | 27 | const renderInputComponent = inputProps => ( 28 |
    29 | 30 |
    31 | ); 32 | 33 | class Search extends Component { 34 | static propTypes = { 35 | callback: PropTypes.func.isRequired 36 | } 37 | 38 | constructor() { 39 | super(); 40 | 41 | this.state = { 42 | value: '', 43 | suggestions: [], 44 | //isLoading: false 45 | }; 46 | 47 | this.lastRequestId = null; 48 | } 49 | 50 | handleClick = suggestion => { 51 | suggestion.instrument = suggestion.url 52 | suggestion.type = "watchlist" 53 | //console.log(suggestion) 54 | this.props.callback(suggestion); 55 | } 56 | 57 | renderSuggestion = (suggestion) => { 58 | return ( 59 |
    this.handleClick(suggestion)} > 60 |
    {suggestion.symbol}
    61 |
    {suggestion.name}
    62 |
    63 | ); 64 | } 65 | 66 | getMatchingInstruments = (value) => { 67 | const escapedValue = escapeRegexCharacters(value.trim()); 68 | 69 | if (escapedValue === '') { 70 | this.setState({ 71 | //isLoading: false, 72 | suggestions: [] 73 | }); 74 | } 75 | 76 | return fetch(`https://api.robinhood.com/instruments/?query=${value}`, { 77 | method: 'GET', 78 | headers: new Headers({ 'Accept': 'application/json' }) 79 | }) 80 | .then(response => response.json()) 81 | .then(jsonResult => { 82 | this.setState({ 83 | //isLoading: false, 84 | suggestions: jsonResult.results 85 | }); 86 | }) 87 | .catch(function(reason) { 88 | console.log(reason); 89 | }); 90 | } 91 | 92 | onSuggestionSelected= (e)=>{ 93 | this.setState({value:''}) 94 | } 95 | 96 | loadSuggestions=(value)=> { 97 | this.setState({ 98 | //isLoading: true 99 | }); 100 | this.getMatchingInstruments(value); 101 | } 102 | 103 | onChange = (event, { newValue }) => { 104 | this.setState({ 105 | value: newValue 106 | }); 107 | }; 108 | 109 | onSuggestionsFetchRequested = ({ value }) => { 110 | this.loadSuggestions(value); 111 | }; 112 | 113 | onSuggestionsClearRequested = () => { 114 | this.setState({ 115 | suggestions: [] 116 | }); 117 | }; 118 | 119 | render() { 120 | const { value, suggestions } = this.state; 121 | const inputProps = { 122 | placeholder: "SEARCH", 123 | value, 124 | onChange: this.onChange 125 | }; 126 | 127 | return ( 128 |
    129 | 140 |
    141 | ); 142 | } 143 | } 144 | 145 | export default Search 146 | -------------------------------------------------------------------------------- /src/containers/BuyingPower.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { connect } from 'react-redux' 3 | import SectionWrapper from '../components/SectionWrapper' 4 | import '../styles/BuyingPower.css' 5 | 6 | class BuyingPower extends Component { 7 | 8 | getBuyingPower = () => { 9 | const { type, cash_balances, margin_balances } = this.props.account; 10 | if( type === "cash" ) { 11 | return Number(cash_balances.buying_power); 12 | } 13 | else { 14 | let temp = Number(margin_balances.overnight_buying_power) / Number(margin_balances.overnight_ratio); 15 | if( Number(margin_balances.margin_limit) === 0 ) { 16 | return temp; 17 | } 18 | else { 19 | return Math.min(temp, Number(margin_balances.unallocated_margin_cash) ); 20 | } 21 | } 22 | } 23 | 24 | getGoldUsed = () => { 25 | const { type, margin_balances } = this.props.account; 26 | if( type === "cash" ) { 27 | return 0; 28 | } 29 | else { 30 | let margin_limit = Number(margin_balances.margin_limit); 31 | let unallocated_margin_cash = Number(margin_balances.unallocated_margin_cash); 32 | let isGold = ( margin_limit !== 0 )? true : false; 33 | if( isGold && ( margin_limit !== 0 ) ) { 34 | if( margin_limit > unallocated_margin_cash ) { 35 | return margin_limit - unallocated_margin_cash; 36 | } 37 | return 0; 38 | } 39 | return 0; 40 | } 41 | } 42 | 43 | getGoldWithheld = () => { 44 | const { type, margin_balances } = this.props.account; 45 | if( type === "cash" ) { 46 | return 0; 47 | } 48 | else { 49 | let margin_limit = Number(margin_balances.margin_limit); 50 | let cash_held_for_orders = Number(margin_balances.cash_held_for_orders); 51 | let isGold = ( margin_limit !== 0 )? true : false; 52 | if( isGold && ( margin_limit !== 0 ) ) { 53 | return Math.max( 0, ( margin_limit - this.getGoldUsed() - this.getBuyingPower() - cash_held_for_orders ) ) 54 | } 55 | return 0; 56 | } 57 | } 58 | 59 | getRobinhoodGold = () => { 60 | const { type, margin_balances } = this.props.account; 61 | if( type === "cash" ) { 62 | return 0; 63 | } 64 | else { 65 | return Number(margin_balances.margin_limit); 66 | } 67 | } 68 | 69 | render() { 70 | if(!this.props.account) return null; 71 | if(this.props.account.type === "cash"){ 72 | if(!this.props.account.cash_balances) return null; 73 | } 74 | else{ 75 | if(!this.props.account.margin_balances) return null; 76 | } 77 | 78 | return ( 79 | 80 |
    81 |
    82 |
    83 | Robinhood Gold 84 |
    85 |
    86 |
    87 |
    88 | {`$${ this.getRobinhoodGold().toFixed(2) }`} 89 |
    90 |
    91 |
    92 |
    93 | Gold used 94 |
    95 |
    96 | - 97 |
    98 |
    99 | {`$${ this.getGoldUsed().toFixed(2) }`} 100 |
    101 |
    102 |
    103 |
    104 | Gold withheld 105 |
    106 |
    107 | - 108 |
    109 |
    110 | {`$${ this.getGoldWithheld().toFixed(2) }`} 111 |
    112 |
    113 |
    114 |
    115 | Buying power 116 |
    117 |
    118 |
    119 |
    120 | {`$${ this.getBuyingPower().toFixed(2) }`} 121 |
    122 |
    123 | 124 |
    125 |
    126 | ) 127 | } 128 | } 129 | 130 | const mapStateToProps = ({ accountReducer }, ownProps) => { 131 | const { account } = accountReducer; 132 | 133 | return { 134 | account 135 | }; 136 | } 137 | 138 | export default connect(mapStateToProps, null)(BuyingPower) 139 | -------------------------------------------------------------------------------- /src/components/ListContainer.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | 3 | import update from 'react/lib/update'; 4 | 5 | import { DragDropContext, DropTarget } from 'react-dnd'; 6 | import HTML5Backend from 'react-dnd-html5-backend'; 7 | import List from './List'; 8 | 9 | import {flow} from '../utils'; 10 | 11 | import '../styles/ListContainer.css' 12 | 13 | const listTarget = { 14 | drop() { 15 | }, 16 | }; 17 | 18 | class ListContainer extends Component { 19 | static propTypes = { 20 | connectDropTarget: PropTypes.func.isRequired, 21 | 22 | localLists: PropTypes.array.isRequired, 23 | instruments: PropTypes.object.isRequired, 24 | checkLists: PropTypes.array.isRequired, 25 | reorderLocalList: PropTypes.func.isRequired, 26 | reorderLocalLists: PropTypes.func.isRequired, 27 | deleteLocalListFolder: PropTypes.func.isRequired, 28 | renameLocallistFolder: PropTypes.func.isRequired 29 | }; 30 | 31 | constructor(props) { 32 | super(props); 33 | this.state = { 34 | lists:[] 35 | }; 36 | } 37 | 38 | componentDidMount(){ 39 | const { localLists, instruments, checkLists } = this.props; 40 | if(!localLists) return; 41 | 42 | this.setListsData(localLists, instruments, checkLists); 43 | } 44 | 45 | componentWillReceiveProps(nextProps){ 46 | const { localLists, instruments, checkLists } = nextProps; 47 | if(!localLists) return; 48 | 49 | this.setListsData(localLists, instruments, checkLists); 50 | } 51 | 52 | setListsData = (localLists, instruments, checkLists) => { 53 | for(let i=0; i { 62 | let temp = {} 63 | temp.list = tempList.list.filter((instrument)=>{ 64 | for(let i=0; i< checkLists.length; i++){ 65 | if( checkLists[i].list.indexOf(instrument) !== -1 ) { 66 | return false; 67 | } 68 | } 69 | return true; 70 | }) 71 | .map((instrument)=>{ 72 | return instrument 73 | }); 74 | 75 | temp.id = tempList.name; 76 | return temp; 77 | }); 78 | 79 | this.setState({ lists: tempLists }); 80 | } 81 | 82 | noDuplicateName = (name) => { 83 | for (var i = 0; i < this.props.localLists.length; i++) { 84 | if( this.props.localLists[i].name === name ) { 85 | console.log("name duplicate!"); 86 | return false; 87 | } 88 | } 89 | return true; 90 | } 91 | 92 | moveList = (id, atIndex) => { 93 | const { list, index } = this.findList(id); 94 | this.setState(update(this.state, { 95 | lists: { 96 | $splice: [ 97 | [index, 1], 98 | [atIndex, 0, list], 99 | ], 100 | }, 101 | })); 102 | 103 | } 104 | 105 | findList = (id) => { 106 | const { lists } = this.state; 107 | const list = lists.filter(c => c.id === id)[0]; 108 | 109 | return { 110 | list, 111 | index: lists.indexOf(list), 112 | }; 113 | } 114 | 115 | render() { 116 | const { reorderLocalLists, reorderLocalList, deleteLocalListFolder, connectDropTarget, renameLocallistFolder, instruments } = this.props; 117 | const { lists } = this.state; 118 | 119 | return connectDropTarget( 120 |
    121 | {lists.map((localList, index)=>{ 122 | return ( 123 | reorderLocalList(index, list)} 129 | moveList={this.moveList} 130 | findList={this.findList} 131 | reorderLocalLists={reorderLocalLists} 132 | deleteLocalListFolder={()=>deleteLocalListFolder(index)} 133 | renameLocallistFolder={(name)=>renameLocallistFolder(index, name)} 134 | instruments={instruments} 135 | noDuplicateName={this.noDuplicateName} 136 | /> 137 | ) 138 | })} 139 |
    140 | ); 141 | } 142 | } 143 | 144 | export default flow([ 145 | DropTarget("LIST", listTarget, connect => ({ 146 | connectDropTarget: connect.dropTarget(), 147 | })), 148 | DragDropContext(HTML5Backend) 149 | ])(ListContainer); 150 | -------------------------------------------------------------------------------- /src/components/HistoryPriceDisplay.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | import '../styles/HistoryPriceDisplay.css' 3 | import { displayPercentage, printDateOnly, dateDiffInDays } from '../utils' 4 | 5 | class HistoryPriceDisplay extends Component { 6 | static propTypes = { 7 | historicals: PropTypes.array.isRequired, 8 | previous_close: PropTypes.string.isRequired, 9 | selectedButtonName: PropTypes.string.isRequired 10 | } 11 | 12 | render() { 13 | const { 14 | selectedButtonName, historicals, 15 | previous_close, last_trade_price, updated_at, last_extended_hours_trade_price 16 | } = this.props; 17 | 18 | let data = []; 19 | if(selectedButtonName === "1M"){ 20 | historicals.forEach((eachData)=>{ 21 | if(dateDiffInDays(eachData.begins_at) <= 31){ 22 | data.push(eachData); 23 | } 24 | }) 25 | } 26 | else if(selectedButtonName === "3M"){ 27 | historicals.forEach((eachData)=>{ 28 | if(dateDiffInDays(eachData.begins_at) <= 92){ 29 | data.push(eachData); 30 | } 31 | }) 32 | } 33 | else { 34 | data = historicals; 35 | } 36 | 37 | let displayBlock = null; 38 | if(selectedButtonName === "1D"){ 39 | displayBlock = ( 40 |
    41 |

    42 | Number(previous_close))? 44 | "greenUp" 45 | : 46 | (Number(last_trade_price).toFixed(2) === Number(previous_close))? 47 | "whiteNomove":"redDown" 48 | }> 49 | { (Number(last_trade_price) - Number(previous_close) >0)? '+' : (Number(last_trade_price) - Number(previous_close) < 0)? '-' : ''} 50 | { 'US$' } 51 | { Math.abs((Number(last_trade_price) - Number(previous_close)).toFixed(2)) } 52 | { ' (' } 53 | { displayPercentage(last_trade_price, previous_close) } 54 | { ')' } 55 | 56 | 57 | { ` ${printDateOnly(updated_at)}` } 58 | 59 |

    60 | { (last_extended_hours_trade_price)? ( 61 |

    62 | { (Number(last_extended_hours_trade_price) - Number(last_trade_price) >0)? '+' : (Number(last_extended_hours_trade_price) - Number(last_trade_price) < 0)? '-' : ''} 63 | { 'US$' } 64 | { Math.abs((Number(last_extended_hours_trade_price) - Number(last_trade_price)).toFixed(2)) } 65 | { ' (' } 66 | { displayPercentage(Number(last_extended_hours_trade_price)-Number(last_trade_price)+Number(previous_close), previous_close) } 67 | { `) After Hours` } 68 |

    69 | ): null 70 | } 71 |
    72 | ); 73 | } 74 | else if(data[0]) { 75 | let firstDayPrice = data[0].adjusted_open_equity || data[0].close_price ; 76 | //let lastDayPrice = data[data.length-1].close_price || data[data.length-1].adjusted_open_equity; 77 | console.log(last_trade_price); 78 | console.log(data[0]); 79 | displayBlock = ( 80 |

    81 | Number(firstDayPrice))? 83 | "greenUp" 84 | : 85 | (Number(last_trade_price).toFixed(2) === Number(firstDayPrice))? 86 | "whiteNomove":"redDown" 87 | }> 88 | { (Number(last_trade_price) - Number(firstDayPrice) >0)? '+' : (Number(last_trade_price) - Number(firstDayPrice) < 0)? '-' : ''} 89 | { 'US$' } 90 | { Math.abs((Number(last_trade_price) - Number(firstDayPrice)).toFixed(2)) } 91 | { ' (' } 92 | { displayPercentage(last_trade_price, firstDayPrice) } 93 | { ')' } 94 | 95 | 96 | { ` Past ${ 97 | (selectedButtonName === "1W")? "Week" : 98 | (selectedButtonName === "1M")? "Month" : 99 | (selectedButtonName === "3M")? "3M" : 100 | (selectedButtonName === "1Y")? "Year" : 101 | "5 Years" 102 | }` } 103 | 104 |

    105 | ); 106 | } 107 | 108 | 109 | 110 | return ( 111 |
    112 | { displayBlock } 113 |
    114 | ) 115 | } 116 | } 117 | 118 | export default HistoryPriceDisplay 119 | /* 120 | 121 | */ 122 | -------------------------------------------------------------------------------- /src/components/Orders.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | import Modal from 'react-modal' 3 | import OrderDetail from './OrderDetail' 4 | import { capFirst, printDate } from '../utils' 5 | import '../styles/Orders.css' 6 | 7 | const customStyles = { 8 | content : { 9 | top : '50px', 10 | backgroundColor : 'black', 11 | textAlign : 'center', 12 | padding : '0px', 13 | border : '0px solid black', 14 | overflowY : 'auto' 15 | }, 16 | overlay :{ zIndex: 999 } 17 | }; 18 | 19 | class Orders extends Component { 20 | static propTypes = { 21 | historicalsOrders: PropTypes.array.isRequired, 22 | historicalsOrdersNextLink: PropTypes.string.isRequired, 23 | isAskingCurrentOrder: PropTypes.bool.isRequired, 24 | currentOrder: PropTypes.object.isRequired, 25 | currentOrderFailedReason: PropTypes.string.isRequired, 26 | instruments: PropTypes.object.isRequired, 27 | addMoreHistoricalsOrder: PropTypes.func.isRequired, 28 | askCurrentOrder: PropTypes.func.isRequired, 29 | cancelCurrentOrderState: PropTypes.string.isRequired, 30 | forInstrument: PropTypes.bool.isRequired 31 | } 32 | 33 | constructor(props) { 34 | super(props); 35 | this.state = { modalIsOpen: false }; 36 | } 37 | 38 | closeModal = () => { 39 | this.setState({ modalIsOpen: false }); 40 | } 41 | 42 | openModalAndAskCurrentOrder = (orderId) => { 43 | this.props.askCurrentOrder(orderId); 44 | this.setState({ modalIsOpen: true }); 45 | } 46 | 47 | typeName = (trigger, type) => { 48 | let typeName = "" 49 | if(trigger === "immediate" && type === "market") {typeName="Market"} 50 | else if(trigger === "immediate" && type === "limit") {typeName="Limit"} 51 | else if(trigger === "stop" && type === "market") {typeName="Stop Loss"} 52 | else if(trigger === "stop" && type === "limit") {typeName="Stop Limit"} 53 | 54 | return typeName; 55 | } 56 | 57 | render() { 58 | const { historicalsOrders, historicalsOrdersNextLink, 59 | isAskingCurrentOrder, currentOrder, currentOrderFailedReason, 60 | forInstrument, instruments, 61 | addMoreHistoricalsOrder, cancelOrder, cancelCurrentOrderState 62 | } = this.props 63 | 64 | 65 | 66 | let recentOrders = historicalsOrders.map((order, i)=>{ 67 | if(!instruments[order.instrument]) { 68 | return null; 69 | } 70 | return ( 71 |
    this.openModalAndAskCurrentOrder(order.id)} > 72 |
    73 | {((forInstrument)? "" : instruments[order.instrument].symbol+": ")+this.typeName(order.trigger, order.type)+" "+capFirst(order.side)} 74 |
    75 | {printDate(order.updated_at)} 76 |
    77 |
    78 |
    79 | {(order.state === "filled")? 80 | "$"+(Number(order.quantity) * Number(order.average_price)).toFixed(2) 81 | : (order.state === "unconfirmed" || order.state === "confirmed")? 82 | "Placed" : 83 | capFirst(order.state)} 84 |
    85 |
    86 | ) 87 | }) 88 | 89 | return ( 90 |
    91 |
    92 | {(historicalsOrders.length > 0)? recentOrders : "No Orders"} 93 |
    94 | {(historicalsOrdersNextLink)? ( 95 |
    96 | 99 |
    100 | ) : null} 101 | 109 | 117 | 118 |
    119 | ) 120 | } 121 | } 122 | 123 | export default Orders 124 | -------------------------------------------------------------------------------- /src/actions/action_quotes.js: -------------------------------------------------------------------------------- 1 | ////////////QUOTES 2 | export const ADD_HIS_QUOTES = 'ADD_HIS_QUOTES' 3 | export const DELETE_HIS_QUOTES = 'DELETE_HIS_QUOTES' 4 | export const ADD_QUOTE = 'ADD_QUOTE' 5 | export const DELETE_QUOTE = 'DELETE_QUOTE' 6 | export const ADD_MULTIPLE_QUOTES = 'ADD_MULTIPLE_QUOTES' 7 | 8 | export const addHistoricalsQuotes = (symbol, hisType, quotes) => ({ 9 | type: ADD_HIS_QUOTES, 10 | symbol, 11 | hisType, 12 | quotes 13 | }) 14 | 15 | export const deleteHistoricalsQuotes = (symbolHisType) => ({ 16 | type: DELETE_HIS_QUOTES, 17 | symbolHisType 18 | }) 19 | 20 | export const askHistoricalsQuotes = (symbol, span, interval, bounds) => (dispatch, getState) => { 21 | //check if already requested on sameday 22 | if( span !== "day" && getState().quotesReducer.historicalsQuotes[ symbol+span+interval+bounds ] ){ 23 | if( getState().quotesReducer.historicalsQuotes[symbol+span+interval+bounds].timestamp === (new Date()).toISOString().substring(0, 10) ){ 24 | return; 25 | } 26 | } 27 | 28 | return fetch(`https://api.robinhood.com/quotes/historicals/${symbol}/?span=${span}&interval=${interval}&bounds=${bounds}`, { 29 | method: 'GET', 30 | headers: new Headers({ 31 | 'Accept': 'application/json', 32 | 'Authorization': getState().tokenReducer.token 33 | }) 34 | }) 35 | .then(response => response.json()) 36 | .then(jsonResult => { 37 | //parse string to number 38 | //open_price, close_price, high_price, low_price 39 | jsonResult.historicals.forEach((historical, index, theArray)=>{ 40 | theArray[index].open_price= Number(historical.open_price); 41 | theArray[index].close_price= Number(historical.close_price); 42 | theArray[index].high_price= Number(historical.high_price); 43 | theArray[index].low_price= Number(historical.low_price); 44 | //custom 45 | theArray[index].not_reg_close_price = Number(historical.close_price); 46 | theArray[index].reg_close_price= (historical.session !== "reg")? undefined : Number(historical.close_price); 47 | }) 48 | 49 | //add timestamp so dont need to request everytime 50 | jsonResult.timestamp = (new Date()).toISOString().substring(0, 10); 51 | dispatch(addHistoricalsQuotes(symbol, span+interval+bounds, jsonResult)); 52 | }) 53 | .catch(function(reason) { 54 | console.log(reason); 55 | }); 56 | } 57 | 58 | export const addQuote = (symbol, quote) => ({ 59 | type: ADD_QUOTE, 60 | symbol, 61 | quote 62 | }) 63 | 64 | export const deleteQuote = (symbol) => ({ 65 | type: DELETE_QUOTE, 66 | symbol 67 | }) 68 | 69 | export const askQuote = (symbol) => (dispatch, getState) => { 70 | 71 | return fetch(`https://api.robinhood.com/quotes/${symbol}/`, { 72 | method: 'GET', 73 | headers: new Headers({ 74 | 'Accept': 'application/json', 75 | 'Authorization': getState().tokenReducer.token 76 | }) 77 | }) 78 | .then(response => response.json()) 79 | .then(jsonResult => { 80 | dispatch(addQuote(symbol, jsonResult)); 81 | }) 82 | .catch(function(reason) { 83 | console.log(reason); 84 | }); 85 | } 86 | 87 | export const addMultipleQuotes = (quotesArray) => ({ 88 | type: ADD_MULTIPLE_QUOTES, 89 | quotesArray 90 | }) 91 | 92 | export const askMultipleQuotes = () => (dispatch, getState) => { 93 | let symbolArray = Object.keys(getState().instrumentsReducer.instruments).map((instrumentKey)=>{ 94 | return getState().instrumentsReducer.instruments[instrumentKey].symbol; 95 | }); 96 | if(symbolArray.length === 0) { 97 | return; 98 | } 99 | 100 | return fetch(`https://api.robinhood.com/quotes/?symbols=${symbolArray.join(',')}`, { 101 | method: 'GET', 102 | headers: new Headers({ 103 | 'Accept': 'application/json', 104 | 'Authorization': getState().tokenReducer.token 105 | }) 106 | }) 107 | .then(response => response.json()) 108 | .then(jsonResult => { 109 | if(jsonResult.results){ 110 | dispatch(addMultipleQuotes(jsonResult.results)); 111 | } 112 | }) 113 | .catch(function(reason) { 114 | console.log(reason); 115 | }); 116 | } 117 | 118 | export const cleanUpQuotes = () => (dispatch, getState) => { 119 | Object.keys(getState().quotesReducer.quotes).forEach((symbol) => { 120 | if(getState().tabsReducer.keys.indexOf(symbol) === -1) { 121 | dispatch(deleteQuote(symbol)); 122 | } 123 | }); 124 | 125 | //delet his quotes 126 | Object.keys(getState().quotesReducer.historicalsQuotes).forEach((symbolHisType) => { 127 | for(let i=0; i { 14 | let tempList = this.props.list.slice(0); 15 | tempList.push(card); 16 | this.props.reorderLocalList(tempList); 17 | } 18 | 19 | removeCard = (index) => { 20 | let tempList = this.props.list.slice(0); 21 | tempList = [...tempList.slice(0,index), ...tempList.slice(index+1)]; 22 | this.props.reorderLocalList(tempList); 23 | } 24 | 25 | moveCard = (dragIndex, hoverIndex) => { 26 | const { list } = this.props; 27 | const dragCard = list[dragIndex]; 28 | 29 | let tempList = list.slice(0); 30 | tempList[dragIndex] = tempList[hoverIndex]; 31 | tempList[hoverIndex] = dragCard; 32 | this.props.reorderLocalList(tempList); 33 | } 34 | 35 | dataChanged = (listName) => { 36 | // data = { description: "New validated text comes here" } 37 | // Update your model from here 38 | let name = listName.message; 39 | this.props.renameLocallistFolder(name); 40 | } 41 | 42 | customValidateText = (text) => { 43 | return (text.length > 0 && text !== "default" && text !== "Default" && this.props.noDuplicateName(text) ); 44 | } 45 | 46 | render() { 47 | const { list, listName, instruments } = this.props; 48 | const { canDrop, isOver, isDragging, connectDragSource, connectDropTarget, connectDragPreview, deleteLocalListFolder } = this.props; 49 | const isActive = canDrop && isOver; 50 | 51 | const opacity = isDragging ? 0 : 1; 52 | const backgroundColor = isActive ? '#E0F7F1' : '#FFF'; 53 | 54 | return connectDragPreview(connectDropTarget( 55 |
    56 | {connectDragSource( 57 |
    58 |

    59 | {(listName === "default")? ( 60 | capFirst(listName) 61 | ) : ( 62 | 70 | )} 71 | 72 |

    73 | {(listName === "default")? null : ( 74 | 77 | )} 78 |
    79 | )} 80 |
    81 | {list.map((card, i) => { 82 | return ( 83 | 91 | ); 92 | })} 93 |
    94 |
    95 | )); 96 | } 97 | } 98 | 99 | const listSource = { 100 | canDrag(props, monitor) { 101 | /* 102 | if(props.id === "default") { 103 | return false; 104 | } 105 | */ 106 | return true; 107 | }, 108 | 109 | beginDrag(props) { 110 | return { 111 | id: props.id, 112 | originalIndex: props.findList(props.id).index, 113 | lastIndex: props.findList(props.id).index, 114 | type: 'LIST' 115 | }; 116 | }, 117 | 118 | endDrag(props, monitor) { 119 | const { id: droppedId, originalIndex, lastIndex } = monitor.getItem(); 120 | const didDrop = monitor.didDrop(); 121 | 122 | if (!didDrop) { 123 | props.moveList(droppedId, originalIndex); 124 | } 125 | 126 | props.reorderLocalLists(originalIndex, lastIndex); 127 | }, 128 | }; 129 | 130 | const cardListTarget = { 131 | canDrop(props, monitor) { 132 | const { type } = monitor.getItem(); 133 | if(type === "LIST"){ 134 | return false; 135 | } 136 | return true; 137 | }, 138 | 139 | hover(props, monitor) { 140 | const { id: draggedId, type } = monitor.getItem(); 141 | const { id: overId } = props; 142 | /////// props.id === "default" 143 | if(type === "CARD" ) { 144 | return; 145 | } 146 | 147 | if (draggedId !== overId) { 148 | const { index: overIndex } = props.findList(overId); 149 | props.moveList(draggedId, overIndex); 150 | monitor.getItem().lastIndex = overIndex; 151 | } 152 | }, 153 | 154 | drop(props, monitor, component ) { 155 | const { id } = props; 156 | const sourceObj = monitor.getItem(); 157 | 158 | if(sourceObj.type === "LIST") { 159 | return; 160 | } 161 | 162 | if ( id !== sourceObj.listId ) component.pushCard(sourceObj.card); 163 | return { 164 | listId: id 165 | }; 166 | } 167 | }; 168 | 169 | export default flow([ 170 | DropTarget(["CARD", "LIST"], cardListTarget, (connect, monitor) => ({ 171 | connectDropTarget: connect.dropTarget(), 172 | isOver: monitor.isOver(), 173 | canDrop: monitor.canDrop() 174 | })), 175 | DragSource("LIST", listSource, (connect, monitor) => ({ 176 | connectDragSource: connect.dragSource(), 177 | connectDragPreview: connect.dragPreview(), 178 | isDragging: monitor.isDragging(), 179 | })) 180 | ])(List); 181 | -------------------------------------------------------------------------------- /src/actions/action_watchlists.js: -------------------------------------------------------------------------------- 1 | import { askInstrument } from './action_instruments' 2 | import { addLocalWatchlists, addLocalWatchlist, removeLocalWatchlist } from './action_local' 3 | ////////////WATCHLISTS 4 | export const ADD_WATCHLISTS = 'ADD_WATCHLISTS' 5 | export const ADD_MORE_WATCHLISTS = 'ADD_MORE_WATCHLISTS' 6 | export const ADD_WATCHLIST = 'ADD_WATCHLIST' 7 | export const DELETE_WATCHLISTS = 'DELETE_WATCHLISTS' 8 | export const REMOVE_FROM_WATCHLISTS = 'REMOVE_FROM_WATCHLISTS' 9 | export const REMOVE_WATCHLIST = 'REMOVE_WATCHLIST' 10 | export const ASKING_WATCHLISTS = 'ASKING_WATCHLISTS' 11 | export const ASKING_WATCHLISTS_FAILED = 'ASKING_WATCHLISTS_FAILED' 12 | 13 | export const askingWatchlistsFailed = (error) => ({ 14 | type: ASKING_WATCHLISTS_FAILED, 15 | error 16 | }) 17 | 18 | export const askingWatchlists = () => ({ 19 | type: ASKING_WATCHLISTS 20 | }) 21 | 22 | export const addWatchlists = watchlists => ({ 23 | type: ADD_WATCHLISTS, 24 | watchlists 25 | }) 26 | 27 | export const addMoreWatchlists = watchlists => ({ 28 | type: ADD_MORE_WATCHLISTS, 29 | watchlists 30 | }) 31 | 32 | export const addWatchlist = watchlist => ({ 33 | type: ADD_WATCHLIST, 34 | watchlist 35 | }) 36 | 37 | export const deleteWatchlists = () => ({ 38 | type: DELETE_WATCHLISTS 39 | }) 40 | 41 | export const askWatchlists = (...theArgs) => (dispatch, getState) => { 42 | let link = (theArgs.length === 0)? "https://api.robinhood.com/watchlists/Default/" : theArgs[0]; 43 | dispatch(askingWatchlists()); 44 | return fetch(link, { 45 | method: 'GET', 46 | headers: new Headers({ 47 | 'Accept': 'application/json', 48 | 'Authorization': getState().tokenReducer.token 49 | }) 50 | }) 51 | .then(response => response.json()) 52 | .then(jsonResult => { 53 | if(jsonResult.hasOwnProperty("results")){ 54 | if(theArgs.length === 0){ 55 | dispatch(addWatchlists(jsonResult.results)); 56 | dispatch(addLocalWatchlists(jsonResult.results.map((instrument)=>{ 57 | return instrument.instrument 58 | }))); 59 | jsonResult.results.forEach((instrument)=>{ 60 | if(!getState().instrumentsReducer.instruments[instrument.instrument]){ 61 | dispatch(askInstrument(instrument.instrument)); 62 | } 63 | }); 64 | } 65 | else { 66 | console.log("more watchlists!") 67 | dispatch(addMoreWatchlists(jsonResult.results)); 68 | if( !jsonResult.next ){ 69 | dispatch(addLocalWatchlists([...getState().watchlistsReducer.watchlists, ...jsonResult.results].map((instrument)=>{ 70 | return instrument.instrument 71 | }))); 72 | } 73 | jsonResult.results.forEach((instrument)=>{ 74 | if(!getState().instrumentsReducer.instruments[instrument.instrument]){ 75 | dispatch(askInstrument(instrument.instrument)); 76 | } 77 | }); 78 | } 79 | 80 | if(jsonResult.next){ 81 | dispatch(askWatchlists(jsonResult.next)); 82 | } 83 | } 84 | else { 85 | dispatch(askingWatchlistsFailed("something not right")); 86 | } 87 | }) 88 | .catch(function(reason) { 89 | console.log(reason); 90 | dispatch(askingWatchlistsFailed(reason)); 91 | }); 92 | } 93 | 94 | export const addToWatchlists = (instrumentSymbol) => (dispatch, getState) => { 95 | var form = new FormData(); 96 | form.append('symbols', instrumentSymbol); 97 | 98 | return fetch(`https://api.robinhood.com/watchlists/Default/bulk_add/`, { 99 | method: 'POST', 100 | headers: new Headers({ 101 | 'Accept': 'application/json', 102 | 'Authorization': getState().tokenReducer.token 103 | }), 104 | body: form 105 | }) 106 | .then(response => response.json()) 107 | .then(jsonResult => { 108 | if(jsonResult[0].created_at){ 109 | dispatch(addWatchlist(jsonResult[0])); 110 | dispatch(addLocalWatchlist(jsonResult[0].instrument)); 111 | } 112 | }) 113 | .catch(function(reason) { 114 | console.log(reason); 115 | }); 116 | } 117 | 118 | export const removeInstrumentInWatchlist = instrumentIndex => ({ 119 | type: REMOVE_WATCHLIST, 120 | instrumentIndex 121 | }) 122 | 123 | export const removeWatchlist = (instrumentId) => (dispatch, getState) => { 124 | let tempWatchlists = getState().watchlistsReducer.watchlists.slice(0); 125 | let instrument = `https://api.robinhood.com/instruments/${instrumentId}/`; 126 | let instrumentIndex = -1; 127 | 128 | for(let i=0; i (dispatch, getState) =>{ 140 | return fetch(`https://api.robinhood.com/watchlists/Default/${instrumentId}`, { 141 | method: 'DELETE', 142 | headers: new Headers({ 143 | 'Accept': 'application/json', 144 | 'Authorization': getState().tokenReducer.token 145 | }) 146 | }) 147 | .then(response => { 148 | dispatch(removeLocalWatchlist(instrumentId)); 149 | dispatch(removeWatchlist(instrumentId)); 150 | }) 151 | .catch(function(reason) { 152 | console.log(reason); 153 | }); 154 | } 155 | -------------------------------------------------------------------------------- /src/containers/RightPanel.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | import { connect } from 'react-redux' 3 | import {Tab, Tabs} from 'react-draggable-tab' 4 | import { deleteTab, reorderTab, selectTab } from '../actions' 5 | import InstrumentPage from './InstrumentPage' 6 | import PortfolioPage from './PortfolioPage' 7 | import HistoryPage from './HistoryPage' 8 | import PriceAlertPage from './PriceAlertPage' 9 | import PendingOrdersPage from './PendingOrdersPage' 10 | import '../styles/Tabs.css' 11 | 12 | const tabsClassNames = { 13 | tabWrapper: 'myWrapper', 14 | tabBar: 'myTabBar', 15 | tabBarAfter: 'myTabBarAfter', 16 | tab: 'myTab', 17 | tabTitle: 'myTabTitle', 18 | tabCloseIcon: 'tabCloseIcon', 19 | tabBefore: 'myTabBefore', 20 | tabAfter: 'myTabAfter' 21 | }; 22 | 23 | const tabsStyles = { 24 | tabWrapper: { 25 | position: 'relative', 26 | top: '0px', 27 | height: 'auto', 28 | marginTop: '0px', 29 | backgroundColor: 'none'//'#076258' 30 | }, 31 | tabBar: {paddingRight: "0"}, 32 | tabTitle: {}, 33 | tabTitleActive:{marginTop: '4px'}, 34 | tabCloseIcon: {marginTop: '4px',opacity: '1',right: '5px', filter:'none', color:'rgb(170, 170, 170)', textShadow:'none'}, 35 | tabCloseIconOnHover:{backgroundColor: 'none', color: 'white'}, 36 | tab: {marginLeft: "2px", backgroundImage: '', backgroundColor: 'teal'}, 37 | tabBefore: {display:'none'},//backgroundImage: 'linear-gradient(#343434, #222222)' 38 | tabAfter: {display:'none'}, 39 | tabOnHover: {backgroundImage:''}, 40 | tabActive: {backgroundImage: '', backgroundColor: '#40C9BD', color:'white'}, 41 | tabBeforeActive: {display:'none'},//backgroundImage: 'linear-gradient(#454545, #333333)' 42 | tabAfterActive: {display:'none'}, 43 | tabBarAfter: {height:'0px', borderBottom:'0px'} 44 | }; 45 | 46 | class RightPanel extends Component { 47 | static propTypes = { 48 | tabs: PropTypes.object.isRequired, 49 | keys: PropTypes.array.isRequired, 50 | selectedKey: PropTypes.string.isRequired, 51 | width: PropTypes.string.isRequired 52 | } 53 | 54 | handleTabSelect = (e, key, currentTabs) => { 55 | this.props.selectTab(key); 56 | } 57 | 58 | handleTabClose = (e, key, currentTabs) => { 59 | let newKeys = currentTabs.map( tab => tab.key ); 60 | this.props.deleteTab(key, newKeys); 61 | } 62 | 63 | handleTabPositionChange = (e, key, currentTabs) => { 64 | let newKeys = currentTabs.map( tab => tab.key ); 65 | this.props.reorderTab(newKeys); 66 | } 67 | 68 | render() { 69 | const { tabs, keys, selectedKey } = this.props; 70 | let newTabs = keys.map((key)=>{ 71 | let isCurrent = (selectedKey === tabs[key].key); 72 | 73 | if(tabs[key].type === "watchlist" || tabs[key].type === "position"){ 74 | return ( 75 | 76 | 82 | 83 | ); 84 | } 85 | else if (tabs[key].type === "portfolio") { 86 | return ( 87 | 88 | 89 | 90 | ); 91 | } 92 | else if (tabs[key].type === "history") { 93 | return ( 94 | 95 | 96 | 97 | ); 98 | } 99 | else if(tabs[key].type === "priceAlert") { 100 | return ( 101 | 102 | 103 | 104 | ); 105 | } 106 | else if(tabs[key].type === "pendingOrders") { 107 | return ( 108 | 109 | 110 | 111 | ); 112 | } 113 | else{ 114 | return ( 115 | 116 |
    NOTHING HERE
    117 |
    118 | ); 119 | } 120 | }); 121 | 122 | return ( 123 | 132 | ) 133 | } 134 | } 135 | 136 | const mapStateToProps = ({ tabsReducer }) => { 137 | const { tabs, keys, selectedKey } = tabsReducer; 138 | return { tabs, keys, selectedKey } 139 | } 140 | 141 | const mapDispatchToProps = (dispatch, ownProps) => ({ 142 | selectTab: (key) => { 143 | dispatch(selectTab(key)); 144 | }, 145 | deleteTab: (key, newKeys) => { 146 | dispatch(deleteTab(key, newKeys)); 147 | }, 148 | reorderTab: (newKeys) => { 149 | dispatch(reorderTab(newKeys)); 150 | } 151 | }) 152 | 153 | export default connect(mapStateToProps, mapDispatchToProps)(RightPanel) 154 | 155 | /* 156 | shortCutKeys={ 157 | { 158 | 'close': ['alt+command+w', 'alt+ctrl+w'], 159 | 'create': ['alt+command+t', 'alt+ctrl+t'], 160 | 'moveRight': ['alt+command+tab', 'alt+ctrl+tab'], 161 | 'moveLeft': ['shift+alt+command+tab', 'shift+alt+ctrl+tab'] 162 | } 163 | } 164 | */ 165 | -------------------------------------------------------------------------------- /src/reducers/reducer_orders.js: -------------------------------------------------------------------------------- 1 | import * as actions from '../actions'; 2 | 3 | const ordersReducer = (state = { 4 | historicalsOrders: [], 5 | historicalsOrdersNextLink: "", 6 | isAskingCurrentOrder: false, 7 | currentOrder: {}, 8 | currentOrderFailedReason: "", 9 | /////////CancelCurrentOrderState: noteven, ing, failed, succeeded 10 | cancelCurrentOrderState: "noteven", 11 | cancelFailedReason: "", 12 | placingOrder: false, 13 | orderPlacedResult: "", 14 | pendingOrders: [], 15 | /* 16 | symbol:{nextLink:"", orders:[]} 17 | */ 18 | ownHistoricalsOrders: {} 19 | }, action) => { 20 | switch (action.type) { 21 | case actions.ORDERS_DELETE_ORDERS: 22 | return { 23 | historicalsOrders: [], 24 | historicalsOrdersNextLink: "", 25 | isAskingCurrentOrder: false, 26 | currentOrder: {}, 27 | currentOrderFailedReason: "", 28 | cancelCurrentOrderState: "noteven", 29 | cancelFailedReason: "", 30 | placingOrder: false, 31 | orderPlacedResult: "", 32 | pendingOrders: [], 33 | ownHistoricalsOrders: {} 34 | } 35 | case actions.ORDERS_REMOVE_FROM_PENDING_ORDERS: 36 | let tempObj = Object.assign({}, state.pendingOrders); 37 | delete tempObj[action.orderID]; 38 | return { 39 | ...state, 40 | pendingOrders: tempObj 41 | } 42 | case actions.ORDERS_ADD_TO_PENDING_ORDERS: 43 | tempObj = {} 44 | tempObj[action.order.id] = action.order; 45 | return { 46 | ...state, 47 | pendingOrders: Object.assign({}, state.pendingOrders, tempObj) 48 | } 49 | case actions.ORDERS_RESET_PLACE_ORDER_RELATED: 50 | return { 51 | ...state, 52 | placingOrder: false, 53 | orderPlacedResult: "" 54 | } 55 | case actions.ORDERS_PLACING_ORDER: 56 | return { 57 | ...state, 58 | placingOrder: true, 59 | orderPlacedResult: "" 60 | } 61 | case actions.ORDERS_ORDER_PLACED: 62 | return { 63 | ...state, 64 | placingOrder: false, 65 | orderPlacedResult: "succeeded" 66 | } 67 | case actions.ORDERS_ORDER_DIDNT_PLACE: 68 | return { 69 | ...state, 70 | placingOrder: false, 71 | orderPlacedResult: action.reason 72 | } 73 | case actions.ORDERS_CANCELLING_CURRENT_ORDER: 74 | return { 75 | ...state, 76 | cancelCurrentOrderState: "ing", 77 | cancelFailedReason: "" 78 | } 79 | case actions.ORDERS_CANCEL_CURRENT_ORDER_FAILED: 80 | return { 81 | ...state, 82 | cancelCurrentOrderState: "failed", 83 | cancelFailedReason: action.reason 84 | } 85 | case actions.ORDERS_CANCEL_CURRENT_ORDER_SUCCEEDED: 86 | return { 87 | ...state, 88 | cancelCurrentOrderState: "succeeded" 89 | } 90 | case actions.ORDERS_DELETE_HIS__ORDERS_NEXT_LINK: 91 | return { 92 | ...state, 93 | historicalsOrdersNextLink: "" 94 | } 95 | case actions.ORDERS_REFILL_HIS_ORDERS: 96 | return { 97 | ...state, 98 | historicalsOrders: action.orders, 99 | historicalsOrdersNextLink: (action.next)? action.next : "" 100 | } 101 | case actions.ORDERS_ADD_HIS_ORDERS: 102 | return { 103 | ...state, 104 | historicalsOrders: state.historicalsOrders.concat(action.orders), 105 | historicalsOrdersNextLink: (action.next)? action.next : "" 106 | } 107 | case actions.ORDERS_DELETE_HIS_ORDERS: 108 | return { 109 | ...state, 110 | historicalsOrders: [], 111 | historicalsOrdersNextLink: "" 112 | } 113 | case actions.ORDERS_ASKING_CURRENT_ORDER: 114 | return { 115 | ...state, 116 | isAskingCurrentOrder: true, 117 | currentOrder: {}, 118 | currentOrderFailedReason: '', 119 | cancelCurrentOrderState: "noteven", 120 | cancelFailedReason: "" 121 | } 122 | case actions.ORDERS_ASK_CURRENT_ORDER_FAILED: 123 | return { 124 | ...state, 125 | isAskingCurrentOrder: false, 126 | currentOrder: {}, 127 | currentOrderFailedReason: action.reason 128 | } 129 | case actions.ORDERS_ADD_CURRENT_ORDER: 130 | return { 131 | ...state, 132 | isAskingCurrentOrder: false, 133 | currentOrder: action.order, 134 | currentOrderFailedReason: '' 135 | } 136 | case actions.ORDERS_REFILL_OWN_HIS_ORDERS: 137 | tempObj = {}; 138 | tempObj[action.symbol] = {}; 139 | tempObj[action.symbol].orders = action.orders; 140 | tempObj[action.symbol].nextLink = action.nextLink; 141 | return { 142 | ...state, 143 | ownHistoricalsOrders: Object.assign({}, state.ownHistoricalsOrders, tempObj) 144 | } 145 | case actions.ORDERS_ADD_OWN_HIS_ORDERS: 146 | tempObj = {}; 147 | tempObj[action.symbol] = {}; 148 | tempObj[action.symbol].orders = state.ownHistoricalsOrders[action.symbol].orders.concat(action.orders); 149 | tempObj[action.symbol].nextLink = action.nextLink; 150 | return { 151 | ...state, 152 | ownHistoricalsOrders: Object.assign({}, state.ownHistoricalsOrders, tempObj) 153 | } 154 | case actions.ORDERS_DELETE_OWN_HIS_ORDERS: 155 | let tempOwnHistoricalsOrders = Object.assign({}, state.ownHistoricalsOrders); 156 | delete tempOwnHistoricalsOrders[action.symbol]; 157 | return { 158 | ...state, 159 | ownHistoricalsOrders: tempOwnHistoricalsOrders 160 | } 161 | default: 162 | return state 163 | } 164 | } 165 | 166 | export default ordersReducer 167 | -------------------------------------------------------------------------------- /src/reducers/reducer_local.js: -------------------------------------------------------------------------------- 1 | import * as actions from '../actions'; 2 | 3 | const localReducer = (state = { 4 | localPositions: [], 5 | localWatchlists: [] 6 | }, action) => { 7 | switch (action.type) { 8 | case actions.DELETE_LOCAL: 9 | return { 10 | localPositions: [], 11 | localWatchlists: [] 12 | } 13 | case actions.TOGGLE_LOCAL_WATCHLIST: 14 | let tempLocalWatchlists = state.localWatchlists.slice(0); 15 | tempLocalWatchlists[action.index].open = !state.localWatchlists[action.index].open; 16 | return { 17 | ...state, 18 | localWatchlists: tempLocalWatchlists 19 | } 20 | case actions.RENAME_WATCHLIST_FOLDER: 21 | tempLocalWatchlists = state.localWatchlists.slice(0); 22 | tempLocalWatchlists[action.index].name = action.name; 23 | return { 24 | ...state, 25 | localWatchlists: tempLocalWatchlists 26 | } 27 | case actions.ADD_WATCHLIST_FOLDER: 28 | return { 29 | ...state, 30 | localWatchlists: [...state.localWatchlists, { name: action.name, open: false, list: action.list }] 31 | } 32 | case actions.CONCAT_LIST_TO_LOCAL_WATCHLIST : 33 | tempLocalWatchlists = state.localWatchlists.slice(0); 34 | tempLocalWatchlists[action.index].list = tempLocalWatchlists[action.index].list.concat(action.list); 35 | return { 36 | ...state, 37 | localWatchlists: tempLocalWatchlists 38 | } 39 | case actions.DELETE_WATCHLIST_FOLDER: 40 | return { 41 | ...state, 42 | localWatchlists: [ 43 | ...state.localWatchlists.slice(0, action.index), 44 | ...state.localWatchlists.slice(action.index + 1) 45 | ] 46 | } 47 | case actions.REMOVE_LOCAL_WATCHLIST: 48 | tempLocalWatchlists = state.localWatchlists.slice(0); 49 | tempLocalWatchlists[action.listIndex].list = [ 50 | ...tempLocalWatchlists[action.listIndex].list.slice(0, action.instrumentIndex), 51 | ...tempLocalWatchlists[action.listIndex].list.slice(action.instrumentIndex+1) 52 | ]; 53 | return { 54 | ...state, 55 | localWatchlists: tempLocalWatchlists, 56 | } 57 | case actions.REORDER_LOCAL_WATCHLIST: 58 | tempLocalWatchlists = state.localWatchlists.slice(0); 59 | tempLocalWatchlists[action.listIndex].list = action.list; 60 | return { 61 | ...state, 62 | localWatchlists: tempLocalWatchlists 63 | } 64 | case actions.REORDER_LOCAL_WATCHLISTS: 65 | tempLocalWatchlists = state.localWatchlists.slice(0); 66 | let firstItem = tempLocalWatchlists[action.aI]; 67 | tempLocalWatchlists[action.aI] = state.localWatchlists[action.bI]; 68 | tempLocalWatchlists[action.bI] = firstItem; 69 | 70 | return { 71 | ...state, 72 | localWatchlists: tempLocalWatchlists 73 | } 74 | ////////////////////////////////////////////////////////////////////////////////////////////////// 75 | case actions.TOGGLE_LOCAL_POSITION: 76 | let tempLocalPositions = state.localPositions.slice(0); 77 | tempLocalPositions[action.index].open = !state.localPositions[action.index].open 78 | return { 79 | ...state, 80 | localPositions: tempLocalPositions 81 | } 82 | case actions.RENAME_POSITION_FOLDER: 83 | tempLocalPositions = state.localPositions.slice(0); 84 | tempLocalPositions[action.index].name = action.name; 85 | return { 86 | ...state, 87 | localPositions: tempLocalPositions 88 | } 89 | case actions.ADD_POSITION_FOLDER: 90 | return { 91 | ...state, 92 | localPositions: [...state.localPositions, {name: action.name, open: false, list: action.list }] 93 | } 94 | case actions.CONCAT_LIST_TO_LOCAL_POSITION : 95 | tempLocalPositions = state.localPositions.slice(0); 96 | tempLocalPositions[action.index].list = tempLocalPositions[action.index].list.concat(action.list); 97 | return { 98 | ...state, 99 | localPositions: tempLocalPositions 100 | } 101 | case actions.DELETE_POSITION_FOLDER: 102 | return { 103 | ...state, 104 | localPositions: [ 105 | ...state.localPositions.slice(0, action.index), 106 | ...state.localPositions.slice(action.index + 1) 107 | ] 108 | } 109 | case actions.REMOVE_LOCAL_POSITION: 110 | tempLocalPositions = state.localPositions.slice(0); 111 | tempLocalPositions[action.listIndex].list = [ 112 | ...tempLocalPositions[action.listIndex].list.slice(0, action.instrumentIndex), 113 | ...tempLocalPositions[action.listIndex].list.slice(action.instrumentIndex+1) 114 | ]; 115 | return { 116 | ...state, 117 | localPositions: tempLocalPositions, 118 | } 119 | case actions.REORDER_LOCAL_POSITION: 120 | tempLocalPositions = state.localPositions.slice(0); 121 | tempLocalPositions[action.listIndex].list = action.list; 122 | return { 123 | ...state, 124 | localPositions: tempLocalPositions 125 | } 126 | case actions.REORDER_LOCAL_POSITIONS: 127 | tempLocalPositions = state.localPositions.slice(0); 128 | firstItem = tempLocalPositions[action.aI]; 129 | tempLocalPositions[action.aI] = state.localPositions[action.bI]; 130 | tempLocalPositions[action.bI] = firstItem; 131 | 132 | return { 133 | ...state, 134 | localPositions: tempLocalPositions 135 | } 136 | ////////////////////////////////////////////////////////////////////////////////////////////////// 137 | default: 138 | return state 139 | } 140 | } 141 | 142 | export default localReducer 143 | -------------------------------------------------------------------------------- /src/actions/action_portfolios.js: -------------------------------------------------------------------------------- 1 | ////////////PORTFOLIOS 2 | export const ADD_HIS_PORTFOLIOS = 'ADD_HIS_PORTFOLIOS' 3 | export const DELETE_HIS_PORTFOLIOS = 'DELETE_HIS_PORTFOLIOS' 4 | export const ADD_PORTFOLIOS = 'ADD_PORTFOLIOS' 5 | export const DELETE_PORTFOLIOS = 'DELETE_PORTFOLIOS' 6 | 7 | export const deletePortfolios = () => ({ 8 | type: DELETE_PORTFOLIOS 9 | }) 10 | 11 | export const addHistoricalsPortfolios = (hisType, portfolios) => ({ 12 | type: ADD_HIS_PORTFOLIOS, 13 | hisType, 14 | portfolios 15 | }) 16 | 17 | export const deleteHistoricalsPortfolios = (hisType) => ({ 18 | type: DELETE_HIS_PORTFOLIOS, 19 | hisType 20 | }) 21 | 22 | export const askHistoricalsPortfolios = (span, interval, bounds) => (dispatch, getState) => { 23 | 24 | if( span !== "day" && getState().portfoliosReducer.historicalsPortfolios[span+interval] ){ 25 | if( getState().portfoliosReducer.historicalsPortfolios[span+interval].timestamp === (new Date()).toISOString().substring(0, 10) ){ 26 | console.log("same day no need to request!"); 27 | return; 28 | } 29 | } 30 | 31 | return fetch(`https://api.robinhood.com/portfolios/historicals/${getState().accountReducer.accountNumber}?span=${span}&interval=${interval}&bounds=${bounds}`, { 32 | method: 'GET', 33 | headers: new Headers({ 34 | 'Accept': 'application/json', 35 | 'Authorization': getState().tokenReducer.token 36 | }) 37 | }) 38 | .then(response => response.json()) 39 | .then(jsonResult => { 40 | if(jsonResult.equity_historicals){ 41 | //parse string to number 42 | //adjusted_close_equity, adjusted_open_equity, close_market_value, open_market_value, net_return, open_equity, close_equity 43 | jsonResult.equity_historicals.forEach((historical, index, theArray)=>{ 44 | theArray[index].adjusted_close_equity = Number(historical.adjusted_close_equity) 45 | theArray[index].adjusted_open_equity = Number(historical.adjusted_open_equity) 46 | theArray[index].close_market_value = Number(historical.close_market_value) 47 | theArray[index].open_market_value = Number(historical.open_market_value) 48 | theArray[index].net_return = Number(historical.net_return) 49 | theArray[index].open_equity = Number(historical.open_equity) 50 | theArray[index].close_equity = Number(historical.close_equity) 51 | //custom 52 | theArray[index].not_reg_close_equity = Number(historical.adjusted_close_equity); 53 | theArray[index].reg_close_equity = (historical.session !== "reg")? undefined : Number(historical.adjusted_close_equity); 54 | }) 55 | //add timestamp so dont need to request everytime 56 | jsonResult.timestamp = (new Date()).toISOString().substring(0, 10); 57 | dispatch(addHistoricalsPortfolios(span+interval, jsonResult)); 58 | } 59 | else { 60 | console.log(jsonResult); 61 | } 62 | }) 63 | .catch(function(reason) { 64 | console.log(reason); 65 | }); 66 | } 67 | 68 | export const addPortfolios = (portfolios) => ({ 69 | type: ADD_PORTFOLIOS, 70 | portfolios 71 | }) 72 | 73 | export const askPortfolios = () => (dispatch, getState) => { 74 | return fetch(`https://api.robinhood.com/portfolios/`, { 75 | method: 'GET', 76 | headers: new Headers({ 77 | 'Accept': 'application/json', 78 | 'Authorization': getState().tokenReducer.token 79 | }) 80 | }) 81 | .then(response => response.json()) 82 | .then(jsonResult => { 83 | if(jsonResult.results.length > 0){ 84 | let res = {} 85 | //parse string to number 86 | res.unwithdrawable_grants = Number(jsonResult.results[0].unwithdrawable_grants) 87 | res.account = jsonResult.results[0].account 88 | res.excess_maintenance_with_uncleared_deposits = Number(jsonResult.results[0].excess_maintenance_with_uncleared_deposits) 89 | res.url = jsonResult.results[0].url 90 | res.excess_maintenance = Number(jsonResult.results[0].excess_maintenance) 91 | res.market_value = Number(jsonResult.results[0].market_value) 92 | res.withdrawable_amount = Number(jsonResult.results[0].withdrawable_amount) 93 | res.last_core_market_value = Number(jsonResult.results[0].last_core_market_value) 94 | res.unwithdrawable_deposits = Number(jsonResult.results[0].unwithdrawable_deposits) 95 | //can be null 96 | res.extended_hours_equity = jsonResult.results[0].extended_hours_equity 97 | res.excess_margin = Number(jsonResult.results[0].excess_margin) 98 | res.excess_margin_with_uncleared_deposits = Number(jsonResult.results[0].excess_margin_with_uncleared_deposits) 99 | res.equity = Number(jsonResult.results[0].equity) 100 | res.last_core_equity = Number(jsonResult.results[0].last_core_equity) 101 | res.adjusted_equity_previous_close = Number(jsonResult.results[0].adjusted_equity_previous_close) 102 | res.equity_previous_close = Number(jsonResult.results[0].equity_previous_close) 103 | res.start_date = jsonResult.results[0].start_date 104 | ////can be null 105 | res.extended_hours_market_value = jsonResult.results[0].extended_hours_market_value 106 | 107 | dispatch(addPortfolios(res)); 108 | } 109 | else { 110 | console.log(jsonResult); 111 | } 112 | }) 113 | .catch(function(reason) { 114 | console.log(reason); 115 | }); 116 | } 117 | 118 | // Refactor - portfolio page 119 | 120 | export const PORTFOLIO_PAGE_UPDATE_QUOTES = 'PORTFOLIO_PAGE_UPDATE_QUOTES' 121 | export const PORTFOLIO_PAGE_SET_SELECTED_BUTTON = 'PORTFOLIO_PAGE_SET_SELECTED_BUTTON' 122 | 123 | export const updatePortfolioPageQuote = (quotes) => ({ 124 | type: PORTFOLIO_PAGE_UPDATE_QUOTES, 125 | quotes 126 | }); 127 | 128 | export const setPortfolioPageSelectedButton = (selectedButton) => ({ 129 | type: PORTFOLIO_PAGE_SET_SELECTED_BUTTON, 130 | selectedButton 131 | }); 132 | -------------------------------------------------------------------------------- /src/containers/PortfolioValue.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { connect } from 'react-redux' 3 | import { PieChart, Pie, Tooltip, Cell } from 'recharts'; 4 | import SectionWrapper from '../components/SectionWrapper' 5 | import PieChartTooltip from '../components/PieChartTooltip' 6 | import '../styles/PortfolioValue.css' 7 | 8 | class PortfolioValue extends Component { 9 | 10 | constructor(props) { 11 | super(props); 12 | this.state = { 13 | chartActiveIndex: -1 14 | }; 15 | } 16 | 17 | handleMouseOver = (data, index) => { 18 | this.setState({ 19 | chartActiveIndex: index, 20 | }); 21 | } 22 | 23 | handleMouseOut = (data, index) => { 24 | this.setState({ 25 | chartActiveIndex: -1, 26 | }); 27 | } 28 | 29 | fillColor = (entry, index) => { 30 | if( entry.name === "Gold used" && index === this.state.chartActiveIndex ) { 31 | return "gold"; 32 | } 33 | else if( entry.name === "Gold used" ) { 34 | return "#FCFC83"; 35 | } 36 | else if(index === this.state.chartActiveIndex) { 37 | return "#40C9BD"; 38 | } 39 | else { 40 | return "#CFF1EE"; 41 | } 42 | } 43 | 44 | render() { 45 | const { positions, quotes, instruments, market_value, equity, account } = this.props; 46 | if(!account) return null; 47 | 48 | let goldUsed = false; 49 | let cashName = "Cash"; 50 | let cashValue = 0; 51 | if( account.type === "cash") { 52 | cashName = "Cash"; 53 | cashValue = (account.cash_balances)? account.cash_balances.buying_power : ""; 54 | } 55 | else { 56 | if(!account.margin_balances) { 57 | cashName = ""; 58 | cashValue = ""; 59 | } 60 | else { 61 | let { margin_limit, unallocated_margin_cash } = account.margin_balances; 62 | goldUsed = ( Number(margin_limit) > Number(unallocated_margin_cash) )? true : false; 63 | 64 | if( Number(margin_limit) === 0 ) { 65 | cashName = "Cash"; 66 | cashValue = unallocated_margin_cash; 67 | } 68 | else if( goldUsed ) { 69 | cashName = "Gold used"; 70 | cashValue = Number(margin_limit) - Number(unallocated_margin_cash); 71 | } 72 | else { 73 | cashName = "Cash"; 74 | cashValue = Number(unallocated_margin_cash) - Number(margin_limit); 75 | } 76 | } 77 | } 78 | 79 | //check all needed data exist 80 | for(let i=0; i< positions.length; i++) { 81 | if(!instruments[positions[i].instrument]) { 82 | return null; 83 | } 84 | let symbol = instruments[positions[i].instrument].symbol; 85 | if(!quotes[symbol]) { 86 | return null; 87 | } 88 | } 89 | 90 | let chartData=[]; 91 | 92 | positions.forEach((position, index)=>{ 93 | // if quantity = 0 dont show 94 | if( Number(position.quantity) === 0 ) { 95 | return; 96 | } 97 | 98 | let symbol = instruments[position.instrument].symbol; 99 | let quantity = Number(position.quantity); 100 | let last_trade_price = Number((quotes[symbol].last_extended_hours_trade_price)? quotes[symbol].last_extended_hours_trade_price : quotes[symbol].last_trade_price ); 101 | let equityValue = quantity*last_trade_price; 102 | 103 | chartData.push({ 104 | name: `$${symbol}`, 105 | quantity: quantity, 106 | last_trade_price: last_trade_price, 107 | value: equityValue, 108 | total: equity 109 | }); 110 | }) 111 | 112 | /* 113 | chartData.push({ 114 | name: cashName, 115 | value: cashValue 116 | }); 117 | */ 118 | console.log(account.type); 119 | return ( 120 | 121 |
    122 |
    123 | 124 | `${name}`} 134 | > 135 | { 136 | chartData.map((entry, index) => ( 137 | 138 | )) 139 | } 140 | 141 | } 144 | /> 145 | 146 |
    147 | 148 |
    149 |
    150 |
    {(goldUsed)? "Portfolio" : "Stocks" }
    151 |
    {(account.type === "cash" || !goldUsed)? `$${Number(market_value).toFixed(2)}` : `$${equity}`}
    152 |
    153 |
    154 |
    {`${cashName}`}
    155 |
    {`$${Number(cashValue).toFixed(2)}`}
    156 |
    157 |
    158 |
    Total market value
    159 |
    {(account.type === "cash" || !goldUsed)? `$${equity}` : `$${Number(market_value).toFixed(2)}`}
    160 |
    161 |
    162 |
    163 |
    164 | ) 165 | } 166 | } 167 | 168 | 169 | 170 | const mapStateToProps = ({ positionsReducer, quotesReducer, instrumentsReducer, accountReducer }, ownProps) => { 171 | const { positions } = positionsReducer; 172 | const { quotes } = quotesReducer; 173 | const { instruments } = instrumentsReducer; 174 | const { account } = accountReducer; 175 | 176 | return { 177 | positions, quotes, instruments, account 178 | }; 179 | } 180 | 181 | export default connect(mapStateToProps, null)(PortfolioValue) 182 | -------------------------------------------------------------------------------- /src/actions/action_positions.js: -------------------------------------------------------------------------------- 1 | import { askInstrument } from './action_instruments' 2 | 3 | import { addLocalPositions } from './action_local' 4 | ////////////POSITIONS 5 | export const ADD_POSITIONS = 'ADD_POSITIONS' 6 | export const ADD_MORE_POSITIONS = 'ADD_MORE_POSITIONS' 7 | export const ADD_POSITION = 'ADD_POSITION' 8 | export const ADD_POSITIONS_WITH_ZERO = 'ADD_POSITIONS_WITH_ZERO' 9 | export const ADD_MORE_POSITIONS_WITH_ZERO = 'ADD_MORE_POSITIONS_WITH_ZERO' 10 | export const DELETE_POSITIONS = 'DELETE_POSITIONS' 11 | export const ASKING_POSITIONS = 'ASKING_POSITIONS' 12 | export const ASKING_POSITIONS_FAILED = 'ASKING_POSITIONS_FAILED' 13 | 14 | export const askingPositionsFailed = (error) => ({ 15 | type: ASKING_POSITIONS_FAILED, 16 | error 17 | }) 18 | 19 | export const askingPositions = () => ({ 20 | type: ASKING_POSITIONS 21 | }) 22 | 23 | export const addPositions = positions => ({ 24 | type: ADD_POSITIONS, 25 | positions 26 | }) 27 | 28 | export const addMorePositions = positions => ({ 29 | type: ADD_MORE_POSITIONS, 30 | positions 31 | }) 32 | 33 | export const deletePositions = () => ({ 34 | type: DELETE_POSITIONS 35 | }) 36 | 37 | export const askPositions = (...theArgs) => (dispatch, getState) => { 38 | let link = (theArgs.length === 0)? `https://api.robinhood.com/positions/?nonzero=true` : theArgs[0]; 39 | dispatch(askingPositions()); 40 | //searcg non zero 41 | return fetch(link, { 42 | method: 'GET', 43 | headers: new Headers({ 44 | 'Accept': 'application/json', 45 | 'Authorization': getState().tokenReducer.token 46 | }) 47 | }) 48 | .then(response => response.json()) 49 | .then(jsonResult => { 50 | if(jsonResult.hasOwnProperty("results")){ 51 | if(theArgs.length === 0){ 52 | dispatch(addPositions(jsonResult.results)); 53 | dispatch(addLocalPositions(jsonResult.results.filter((position) => { 54 | //if quantity = 0 dont show 55 | return ( Number(position.quantity) > 0 ); 56 | }).map((position)=>{ 57 | return position.instrument; 58 | }))); 59 | jsonResult.results.forEach((instrument)=>{ 60 | if(!getState().instrumentsReducer.instruments[instrument.instrument]){ 61 | dispatch(askInstrument(instrument.instrument)); 62 | } 63 | }); 64 | } 65 | else { 66 | console.log("more watchlists!") 67 | dispatch(addMorePositions(jsonResult.results)); 68 | if( !jsonResult.next ){ 69 | dispatch(addLocalPositions([...getState().positionsReducer.positions, ...jsonResult.results].map((position)=>{ 70 | return position.instrument; 71 | }))); 72 | } 73 | jsonResult.results.forEach((instrument)=>{ 74 | if(!getState().instrumentsReducer.instruments[instrument.instrument]){ 75 | dispatch(askInstrument(instrument.instrument)); 76 | } 77 | }); 78 | } 79 | 80 | if(jsonResult.next){ 81 | dispatch(askPositions(jsonResult.next)); 82 | } 83 | } 84 | else { 85 | dispatch(askingPositionsFailed("something not right")); 86 | } 87 | }) 88 | .catch(function(reason) { 89 | console.log(reason); 90 | dispatch(askingPositionsFailed(reason)); 91 | }); 92 | } 93 | 94 | export const addPosition = position => ({ 95 | type: ADD_POSITION, 96 | position 97 | }) 98 | 99 | export const askPosition = (url) => (dispatch, getState) => { 100 | //searcg non zero 101 | return fetch( url, { 102 | method: 'GET', 103 | headers: new Headers({ 104 | 'Accept': 'application/json', 105 | 'Authorization': getState().tokenReducer.token 106 | }) 107 | }) 108 | .then(response => response.json()) 109 | .then(jsonResult => { 110 | if(jsonResult.hasOwnProperty("quantity")){ 111 | dispatch(addPosition(jsonResult)); 112 | } 113 | }) 114 | .catch(function(reason) { 115 | console.log(reason); 116 | }); 117 | } 118 | 119 | export const askPositionWithInstrument = (instrument) => (dispatch, getState) => { 120 | let url = `https://api.robinhood.com/positions/${getState().accountReducer.accountNumber}/${getState().instrumentsReducer.instruments[instrument].id}`; 121 | 122 | return fetch( url, { 123 | method: 'GET', 124 | headers: new Headers({ 125 | 'Accept': 'application/json', 126 | 'Authorization': getState().tokenReducer.token 127 | }) 128 | }) 129 | .then(response => response.json()) 130 | .then(jsonResult => { 131 | if(jsonResult.hasOwnProperty("quantity")){ 132 | dispatch(addPosition(jsonResult)); 133 | } 134 | }) 135 | .catch(function(reason) { 136 | console.log(reason); 137 | }); 138 | } 139 | 140 | export const addPositionsWithZero = positions => ({ 141 | type: ADD_POSITIONS_WITH_ZERO, 142 | positions 143 | }) 144 | 145 | export const addMorePositionsWithZero = positions => ({ 146 | type: ADD_MORE_POSITIONS_WITH_ZERO, 147 | positions 148 | }) 149 | 150 | export const askPositionsWithZero = (...theArgs) => (dispatch, getState) => { 151 | let link = (theArgs.length === 0)? "https://api.robinhood.com/positions/" : theArgs[0]; 152 | //searcg non zero 153 | return fetch(link, { 154 | method: 'GET', 155 | headers: new Headers({ 156 | 'Accept': 'application/json', 157 | 'Authorization': getState().tokenReducer.token 158 | }) 159 | }) 160 | .then(response => response.json()) 161 | .then(jsonResult => { 162 | if(jsonResult.hasOwnProperty("results")){ 163 | if(theArgs.length === 0){ 164 | dispatch(addPositionsWithZero(jsonResult.results)); 165 | jsonResult.results.forEach((instrument)=>{ 166 | if(!getState().instrumentsReducer.instruments[instrument.instrument]){ 167 | dispatch(askInstrument(instrument.instrument)); 168 | } 169 | }); 170 | } 171 | else { 172 | console.log("more PositionsWithZero!") 173 | dispatch(addMorePositionsWithZero(jsonResult.results)); 174 | jsonResult.results.forEach((instrument)=>{ 175 | if(!getState().instrumentsReducer.instruments[instrument.instrument]){ 176 | dispatch(askInstrument(instrument.instrument)); 177 | } 178 | }); 179 | } 180 | 181 | if(jsonResult.next){ 182 | dispatch(askPositionsWithZero(jsonResult.next)); 183 | } 184 | } 185 | }) 186 | .catch(function(reason) { 187 | console.log(reason); 188 | }); 189 | } 190 | --------------------------------------------------------------------------------