Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added Robinhood broker class #79

Closed
wants to merge 0 commits into from
Closed

Added Robinhood broker class #79

wants to merge 0 commits into from

Conversation

rigoorozco
Copy link

In order to make the Robinhood class I modified main.py to not need the broker-uri when Robinhood is selected. I added the 'rh' directory in brokers. This includes the modified Robinhood API and the modified pyEX API so they can be installed with zipline. I also added the google bundle downloader which is still a little buggy.

I've done a good amount of testing to see if all the functionality works, but I could use some help.

if resp.status_code == 200:
return resp.json()
else:
print("Response error %d. Trying again..." % resp.status_code)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use logbook based logger instance throughout the code.

@@ -0,0 +1,34 @@
"""
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you please move the content of this file to robinhood.py? It is not so big, if we remove the comments. The class names are self-explanatory, thus comments are not really needed in my view.

headers = None
auth_token = None

logger = logging.getLogger('Robinhood')
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you please move this out to the top level of the module? Additionally, please use logbook to instantiate - that's the logger used throughout zipline code.

auth_token = None

logger = logging.getLogger('Robinhood')
logger.addHandler(logging.NullHandler())
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure what's the purpose of this. Might be not needed.

def login_prompt(self): #pragma: no cover
"""Prompts user for username and password and calls login() """

username = input("Username: ")
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd really prefer to not to have any interaction.
If we have interaction it is so much harder to automatically start the app.

The login and password should be specified either via environment variables or application parameters.

Copy link
Author

@rigoorozco rigoorozco Nov 28, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll make the user info come from a config file so no interaction is required

req = self.session.post(self.endpoints['logout'])
req.raise_for_status()
except requests.exceptions.HTTPError as err_msg:
warnings.warn('Failed to log out ' + repr(err_msg))
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should use log.warn here

req.raise_for_status()
data = req.json()
except requests.exceptions.HTTPError:
raise RH_exception.InvalidTickerSymbol()
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All HTTP errors will be treated as if we posted invalid symbol - which might not be the case. I'd suggest to do deeper inspection here. E.g. http 500, shall not raise InvalidTickerSymbol.

return data


# We will keep for compatibility until next major release
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Compatibility between what?

raise Exception("Could not generate instrument object: %s " % res.headers)


def quote_data(self, stock=''):
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if empty string as stock default makes any sense here. What does robinhood returns in such case?
Also, stock can be stocks as you expect comma delimited list. Shouldn't we call it stocks and mentioned it in the docs?


#Prompt for stock if not entered
if not stock: #pragma: no cover
stock = input("Symbol: ")
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as above.

"""

#Creates a tuple containing the information we want to retrieve
def append_stock(stock):
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand the purpose of this function, documentation does not help either. Could you please rename it to have a more meaningful name?



def get_quote(self, stock=''):
"""Wrapper for quote_data """
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unhelpful doc string

return self.get_quote_list(stock,'adjusted_previous_close')


def symbol(self, stock=''):
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above: not sure if it makes sense to pass an empty stock symbol



###########################################################################
# GET FUNDAMENTALS
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unhelpful, very intrusive comment.

###########################################################################

def portfolios(self):
"""Returns the user's portfolio data """
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In general: I'd rather not put doc-strings unless it is necessary. This comment does not help to understand what's returned. Please either remove it or make it meaningful.

# PORTFOLIOS DATA
###########################################################################

def portfolios(self):
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this be a property?

return float(self.portfolios()['adjusted_equity_previous_close'])


def equity(self):
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this be a property?

"""Wrapper for portfolios

Returns:
(float): `extended_hours_market_value` value
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

or None...

try:
return float(self.portfolios()['extended_hours_market_value'])
except TypeError:
return None
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if this is nice. At least we should log this failed attempt.

"""Place an order with Robinhood

Notes:
OMFG TEST THIS PLEASE!
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree.

Notes:
OMFG TEST THIS PLEASE!

Just realized this won't work since if type is LIMIT you need to use "price" and if
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wat?

'type': order.lower()
}

#data = 'account=%s&instrument=%s&price=%f&quantity=%d&side=%s&symbol=%s#&time_in_force=gfd&trigger=immediate&type=market' % (
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please do not commit dead code

@@ -0,0 +1,1147 @@
"""Robinhood.py: a collection of utilities for working with Robinhood's Private API """
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay. I just realized that this code has been copy pasted from https://github.com/Jamonek/Robinhood/blob/master/Robinhood/Robinhood.py.

Please don't copy it here, but rather use it as external library.
If you have modifications in it we can create our own fork, but we should not compound libraries into zipline-live.

@@ -0,0 +1,133 @@
import os
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we should include this code as part of robinhood integration. If you need it back please post a new PR.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll leave this out on the next PR

@@ -0,0 +1,234 @@
import requests
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is unclear to me why do we have IEX related code under robinhood. Are those two always used together?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I decided to include IEX to pull in data since I couldn't get it using the Robinhood API. I couldn't get some of these things: last_trade_price, last_trade_size, last_trade_time, total_volume and vwap

I could leave it out, but I'll have an incomplete bar/tick.

return timedelta(seconds=0)
# return self._rh.time_skew

def order(self, asset, amount, limit_price, stop_price, style):
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This class uses the old API. We recently moved to use Blotter-Live, which changes some of the broker's call signatures. Could you please adopt your implementation too?

self.account = account


class ROBINHOODBroker(Broker):
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Most of this code is duplicate of IBBroker. We should consider moving up the shared code to Broker to avoid code duplication. Could you please check what's possible?

@tibkiss
Copy link

tibkiss commented Nov 22, 2017

Thank your for your contribution. Unfortunately additional work needs to be done before we can merge it. I still haven't got through completely, will need at least one-more pass to better understand what's going on. Meanwhile: could you please address my comments, rebase your branch and fix the flake8 & testcase failures? Thank you!

Copy link

@tibkiss tibkiss left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See my posted comments.

@rigoorozco rigoorozco closed this Nov 29, 2017
@coveralls
Copy link

coveralls commented Nov 29, 2017

Coverage Status

Coverage decreased (-0.005%) to 86.435% when pulling 2ae0308 on rigoorozco:master into 016fa8f on zipline-live:master.

@tibkiss tibkiss mentioned this pull request Dec 27, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants