Quantopian & Robinhood - lessons learned & best practices?

I gather from various Quantopian users posts, there is some trickiness to using Robinhood.

For starters, perhaps someone can fill me in on the various Robinhood account types (I find the Robinhood documentation really bad, which is not exactly comforting). For example, I found:

https://support.robinhood.com/hc/en-us/articles/210216813-Account-Types

• Robinhood
• Robinhood Gold
• Cash

And then there is Robinhood Instant (see http://blog.robinhood.com/news/2016/2/22/introducing-robinhood-instant ). Or maybe this option was dropped, or is now just plain Robinhood?

What account types have folks tried, and how did they work out?

How has customer support been with Quantopian and Robinhood?

20 responses

Account types:

1. Robinhood (this is just cash) - The default account that users start out with, After selling any stock you must wait T+3 days until the cash goes back to your buying power. You can buy and sell (daytrade) as much as you want until you run out of buying power, no shorting.

All market orders placed with robinhood are actually limit orders with a limit that is 5% above the current price. This limits your losses on buying an illiquid stock to at most 5%. Essentially, any order you place could be 5% higher so you really have to place orders that are at most 95% of your availible cash. This is not a huge concern but I do it anyways. You can always place another order afterwards for 95% of the remaining 5% cash if you don't want to hold a 5% cash position.

Quantopian communicates with robinhood on a 1 minute time interval. Any order you place will not be reported to you as filled for at least 1 minute so your buying power will be incorrect if you are placing sell orders and buy orders in the same minute using order_target_percent(). There are some subtleties that I will eventually get around to making a post about but for the most part I highly recommend calculating exactly how many shares you want to order, and telling robinhood explicitly using the order() function, never using an order_target() function and placing your sell orders first.

@Luke. Good overview. A couple of comments.

The 'standard' Robinhood accounts are now 'instant' (at least as of my writing this 3/3/2017). Robinhood doesn't call them 'instant' anymore. Everyone who signs up immediately has access to the limited margin features as Luke noted above. (See https://support.robinhood.com/hc/en-us/articles/210216813-Account-Types)

I had a 'standard / instant' account and now have a 'Gold' account. The only difference that I've experienced is that one's buying power get's increased. With both accounts, if I ever tried to place a trade for more than my 'buying power' then the trade was rejected and had a status code of '3' in the Quantopian 'order.status' field. One feature of the Gold accounts is access to after hours trading. However, Quantopian only supports market hour trading so this feature goes unused.

I'd recommend never placing 'market' orders but rather go through the few extra steps to look at the current market price and then place a plain 'order' with the exact number of shares and using a 'style=LimitOrder' and specifying the price. You then don't need to keep track of the 5% cash buffer as Luke mentioned. Just my opinion.

I've never had to contact support so can't comment on that.

All in all, I would HIGHLY recommend Robinhood for anyone just getting into trading. You can easily buy and sell very low volumes without worrying about having your funds eaten up by commissions. Several times my algorithm got into a mode where it would buy a share one day and sell a share the next. I actually made a few cents on the trade but this 'anomaly' in my code would have cost me $1 each trade with IB. Not a huge amount but I'd need to forgo my Starbucks espresso for the day. Thanks Luke & Dan, So I gather that order_target_percent() with a gross leverage of 0.95 could still be a problem under Robinhood (formally known as "Robinhood Instant")? Say I just call order_target_percent() once per day, sometime shortly after the open, once per week (a weekly re-balancing of a portfolio). I should do something fancier? Supposing I have 2 stocks, say XYZ & PDQ. I have 95% in XYZ, and want to go to 95% PDQ. I need to first sell XYZ (or as much as I can in a certain amount of time), and then buy PDQ with whatever cash I have (up to 0.95 gross leverage)? And is the cash even reported back to Quantopian, since presumably the T+3 rule still applies? Or do I need to estimate the virtual cash from the filled orders? I guess I'm confused about the use of limit orders. If I use them, then does Robinhood not worry about my not having 5% cash and will accept/reject orders based on their limits? Is there a fool-proof, hassle-free recipe? I can write a some code that manages things for the worst case of not being able to sell some/all of a stock. Sounds like that's what needs to be done, right? It just seems like a big hassle, but I guess that otherwise, Robinhood would need to come up with the cash and loan it to me. A few more questions: -- Does Robinhood support paper trading, so one could test an algo against their rules, without capital? -- In practice, how does one code for the max "3 day trades in a 5 day rolling window" rule? I'm not so interested in this right now, since per https://support.robinhood.com/hc/en-us/articles/217072366-Pattern-Day-Trading , it is: the purchase and sale of the same security on the same trading day. So, if you sell and then buy a security, or buy and then sell a security in the same day, you’ve executed one day trade. For completeness, though, would one have to track day trades within the algo? Is the current number of day trades, per Robinhood, reported to Quantopian? A side comment regarding the day trading rule is that, for example, one could hold SPY & IVV. To change the exposure to the S&P500 in one day, one could increase SPY in the morning, for example, and then decrease IVV in the afternoon, such that the exposure to the S&P500 varied up, and then down in the same day. For index trading using ETFs, the day trading rule would seem to be antiquated. And of course leveraged and inverse ETFs could be used, too, which allow for intraday adjustments to index exposures that would not qualify as day trading. -- Regarding "All market orders placed with Robinhood are actually limit orders with a limit that is 5% above the current price" so the implication is that Robinhood will reject the order if the available cash would not be able to cover the worst-case scenario. It doesn't say it explicitly on https://support.robinhood.com/hc/en-us/articles/208650386-Order-Types but is it the case that buy limit orders are subject to the same rule? I guess this is what Dan is saying--instead of using the default 5% collar, compute your own at a lower level (but still > 0%). And I guess the code required to get to ~1.0 gross leverage would involve a series of orders until only fractional shares could be purchased? I suppose one could just order-order-order until the orders start getting rejected due to the fact that whole shares can no longer be purchased. Pretty ugly approach, though. Grant, • I should do something fancier?: Yes I think so, but you don't have to if you are willing to accept rejected orders and unfilled/delayed execution or your algorithm is simple enough. See below. • Say I just call order_target_percent() once per day, sometime shortly after the open, once per week order_target_percent() is only a problem because it doesn't respect orders already placed or sell->buy. If you only ran it once a day you'd be fine (but your algo would be slow to rebalance). But usually once a minute is sufficient on liquid stocks, you never know though which is why you have to check for get_open_orders(). The .95 issue exists in the execution, it's not a problem on liquid securities (theoretically). Lets say you trade only SPY, you could use order_target_percent(spy,1.0) and probably be fine, unless liquidity was constrained. Then your market order could actually attempt to place an order for more than your cash balance up to 1.05, because it's actually a limit order of 1.05*price, on accident which would get rejected. If I place a limit order at SPY's current price then there's a chance it dosen't get executed if the price moves. The best solution I've found is to calculate everything by hand using cash=cash*.95 for each order placed. Then the algo can check to see if you're at your allocation and orders are filled, and place another order for spy using cash=cash*.95 effectively giving you 1-(5%x5%)= 99.75% of your initial allocation It will just repeat until it meets your tolerance or the cash remaining isn't enough to purchase 1 more share. This works for me, there may be better ways. • do I need to estimate the virtual cash from the filled orders? No, because all Robinhood accounts are apparently "instant" that I described above, you don't have to track the T+3 days anymore because your cash is added back to your buying power as soon as the sell executes. The buying power is reported to quantopian once per minute but Robinhood's is updated every second (or faster?) any order that is placed for more than your buying power is rejected by Robinhood. So quantopian's buying power (used in all order_target functions) is at most 1 minute behind robinhood. This is why you should never place a buy and a sell order in the same minute, this problem is solved by using if get_open_orders(): return. • If I use them, then does Robinhood not worry about my not having 5% cash and will accept/reject orders based on their limits? Yes, but not for that reason. Robinhood doesn't care about the 5%, they just reject any order that placed that tries to execute using more than your buying power. Limit orders just guarantee your price which means that you won't get worse execution than you are expecting and you won't get rejection because the market price tries to fill higher than the current price. The trade off is you may not get execution at all. It's a trade_off that's no better IMO which is why I use .95 instead of limit orders. • Is there a fool-proof, hassle-free recipe? Not that I've found. But it really doesn't take a whole lot to get it to a "good enough" state. I spend a LOT of time trying to perfect my buying/selling execution because for my algorithm really matters. Also, I make it a lot more complicated than it should be because I cannot abide by a backtest that just uses leverage willy-nilly because it won't give me accurate to life-results. If it goes above 1.0 leverage at any time, even for a millisecond it's not matching live results and money is on the line. One day I plan to make an ultimate ordering setup that people can copy/paste into their algorithms easily. However, that day is not here yet :) • Does Robinhood support paper trading, so one could test an algo against their rules, without capital? No, Robinhood is very WYSIWYG. Their support is non-existant and any questions are redirected to their help website. if you are interested I would put$50 in a robinhood account and test your code out on some penny stocks. It will be eye opening.
• In practice, how does one code for the max "3 day trades in a 5 day rolling window" rule? One day I will post about this, it would take a while to explain it. Just know it's possible to limit your daytrades by this rule in Quantopian and it works but makes your buy/selling logic even more complicated. However, if you just ignore it, robinhood will reject your orders and protect you from becoming getting flagged as PDT. Robinhood isn't smart about it at all. The rule you quoted is literal, what's considered "same day" is reset at market open.

Here's a sample algorithm used in live trading for Robinhood. It uses a fixed basket of ETFs but the approach could be adapted to more general stock orders. A couple of caveats:

1. It sells once early in the day and then buys once later in the day (now that there is cash from the sales). Day trading isn't an issue.
2. It calculates target number of shares for each security based upon weight and current price.
3. It calculates an order qty of shares based upon the difference between current holdings and target number of shares.
4. It will try to sell any stocks if the order qty < 0
5. It will try to buy any stocks if the order qty > 0. It adjusts the target shares based upon current portfolio cash before buying (in case there isn't enough cash for some reason)

All the data for the stocks are stored in the dataframe returned by the pipeline (eg 'order_qty'). I feel keeping all the data in a big 'spreadsheet' makes it easier to manipulate the data. All the orders are placed as limit orders with a limit of the current price.

466
Backtest from to with initial capital
Total Returns
--
Alpha
--
Beta
--
Sharpe
--
Sortino
--
Max Drawdown
--
Benchmark Returns
--
Volatility
--
 Returns 1 Month 3 Month 6 Month 12 Month
 Alpha 1 Month 3 Month 6 Month 12 Month
 Beta 1 Month 3 Month 6 Month 12 Month
 Sharpe 1 Month 3 Month 6 Month 12 Month
 Sortino 1 Month 3 Month 6 Month 12 Month
 Volatility 1 Month 3 Month 6 Month 12 Month
 Max Drawdown 1 Month 3 Month 6 Month 12 Month
'''
Sample trading algorithm for Robinhood
'''

# import pipeline methods
from quantopian.algorithm import attach_pipeline, pipeline_output
from quantopian.pipeline import Pipeline

# import built in factors and filters
import quantopian.pipeline.factors as Factors
import quantopian.pipeline.filters as Filters

# import any datasets we need
from quantopian.pipeline.data.builtin import USEquityPricing

# import numpy and pandas just in case
import numpy as np
import pandas as pd

# Set any algorithm 'constants' you will be using
MIN_CASH = 25.00

# Here we specify the ETFs and their associated weights
MY_ETFS = pd.DataFrame.from_items([
(symbol('SPXL'),[ 0.50]),
(symbol('TMF'), [0.25]),
(symbol('EDZ'), [0.25]),

], columns = ['weight'], orient='index')

def initialize(context):
"""
Called once at the start of the algorithm.
"""

# Create a list of daily orders (used for retrying). Initially it's empty.
context.todays_orders = []

# Set commision model for Robinhood

# Ensure no short trading in Robinhood (just a precaution)
set_long_only()

# Create and attach pipeline to get data
attach_pipeline(my_pipeline(context), name='my_pipeline')

# Try to place orders
schedule_function(enter_sells, date_rules.every_day(), time_rules.market_open(minutes = 10))
schedule_function(enter_buys, date_rules.every_day(), time_rules.market_open(minutes = 30))

# Retry any cancelled orders 3 times for 10 minutes
schedule_function(retry_cancelled_order, date_rules.every_day(), time_rules.market_open(minutes = 35))
schedule_function(retry_cancelled_order, date_rules.every_day(), time_rules.market_open(minutes = 45))
schedule_function(retry_cancelled_order, date_rules.every_day(), time_rules.market_open(minutes = 55))

# Record tracking variables at the end of each day.
schedule_function(my_record_vars, date_rules.every_day(), time_rules.market_close())

def my_pipeline(context):
'''
Define the pipline data columns
'''

# Create filter for just the ETFs we want to trade
universe = Filters.StaticAssets(MY_ETFS.index)

# Create any factors we need
# latest_price is just used in case we don't have current price for an asset
latest_price = Factors.Latest(inputs =[USEquityPricing.close], mask=universe)

return Pipeline(
columns = {
'latest_price' : latest_price,
},
screen = universe,
)

# Clear the list of todays orders and start fresh
# Would like to do this 'context.today_orders.clear()' but not supported
del context.todays_orders[:]

# Get the data
context.output = pipeline_output('my_pipeline')

# Add other columns to the dataframe for storing qty of shares held, etc
context.output = context.output.assign(
held_shares = 0,
target_shares = 0,
order_shares = 0,
target_value = 0.0,
order_value = 0.0,
weight = MY_ETFS.weight,
)

update_stock_data(context, context.output, data)

def enter_sells(context, data):

update_stock_data(context, context.output, data)

# We want to sell anything less than our min number of shares
rules = 'order_value < [email protected]_ADJUST_AMT'
sells = context.output.query(rules).index.tolist()

for stock in sells:
order_id = order(stock,
context.output.get_value(stock, 'order_shares'),
style=LimitOrder(context.output.get_value(stock, 'latest_price'))
)
context.todays_orders.append(order_id)

update_stock_data(context, context.output, data)

# We want to buy anything greater than our min number of shares
rules = 'order_value > @MIN_ADJUST_AMT'

for stock in buys:
order_id = order(stock,
context.output.get_value(stock, 'order_shares'),
style=LimitOrder(context.output.get_value(stock, 'latest_price'))
)
context.todays_orders.append(order_id)

def update_stock_data(context, output_df, data):
# Determine portfolio value we want to call '100%'
target_portfolio_value = context.portfolio.portfolio_value + ROBINHOOD_GOLD_BUYING_POWER - MIN_CASH
context.target_portfolio_value = target_portfolio_value

# Update the shares held for any security we hold
# If we don't hold a security held_shares keeps the default value of 0
for security, position in context.portfolio.positions.items():
output_df.set_value(security, 'held_shares', position.amount)

# Get the latest prices for all our securities
# May want to account for possibility of price being NaN or 0?
output_df.latest_price = data.current(output_df.index, 'price')

# Calculate amounts (note the // operator is the python floor function)
output_df.target_value = output_df.weight * target_portfolio_value
output_df.target_shares = output_df.target_value // output_df.latest_price

output_df.order_shares = output_df.target_shares - output_df.held_shares
output_df.order_value = output_df.order_shares * output_df.latest_price

# If order_shares is positive then we want to buy more
required_cash = (context.output
.query('order_shares > 0')
.order_value.sum(axis=0)
)
net_cash = context.portfolio.cash + ROBINHOOD_GOLD_BUYING_POWER - MIN_CASH

if required_cash < net_cash and net_cash > 0.0:
# We're good to go
pass

elif required_cash > 0.0 and net_cash > 0.0:
# required_cash should always be > 0 but checking anyway
reduce_by_ratio = required_cash / net_cash
context.output.order_shares = (context.output.query('order_shares > 0')
.order_shares // reduce_by_ratio
)
else:
# net cash is negative so don't buy anything
context.output.order_shares = (context.output.query('order_shares > 0')
.order_shares * 0.0
)

# Calculate the new order_value since we changed the order shares
# Do the calc for the whole dataframe but really just need to do the 'order_shares > 0'
context.output.order_value = context.output.order_shares * context.output.latest_price

.query('order_shares >= 0')
.order_value.sum(axis=0)
)
if adjusted_cash >= net_cash:
# Just checking to make sure
log.info('got a problem %f  %f' % (adjusted_cash, net_cash))

def retry_cancelled_order(context, data):
for order_id in context.todays_orders[:]:
my_order = get_order(order_id)
if my_order and my_order.status == 2 :
# The order was somehow cancelled so retry
order_id = order(
my_order.sid,
my_order.amount,
style=LimitOrder(my_order.limit)
)
context.todays_orders.append(order_id)
log.info('order for %i shares of %s cancelled - retrying' % (my_order.amount, my_order.sid))
# Remove the original order (typically can't do but note the [:]
context.todays_orders.remove(order_id)

def my_record_vars(context, data):
"""
Plot variables at the end of each day.
"""

record(cash=context.portfolio.cash)

There was a runtime error.

Thanks Luke & Dan -

I'll ponder this stuff, and eventually write a little code and share it. That's the only way I'll fully absorb the details.

Any idea how Robinhood decides to accept/reject orders based on the buying power? How is it calculated? Is it simply based on the current cash balance? Or are other factors taken into account? If the current price of a stock is $1, and I only have$1 cash, then there's a pretty high probability that the trade will go through at >\$1, and the cash balance will go slightly negative. I'm figuring that they must build in some margin for themselves, since once the trade is done, then it is done, right? I guess that's the point of using limit orders at the current price.

Does Robinhood support paper trading, so one could test an algo against their rules, without capital?

I guess I misunderstood the question originally, now I see it means paper trading on Robinhood itself. The answer 'No' was correct. As for Robinhood testing on Quantopian, it's a mixed bag:
Yes and no. You can do papertrading on a copy of a live/real Robinhood algo (from the day you started it on Robinhood) or any algo being tested/intended for Robinhood however if it goes into margin like the one above (~20%), results in the chart won't be realistic. If Q would just implement set_nonmargin(), they would be also modeling nonmargin accounts of the real world rather than just margin accounts as it is currenly. And then there's also the issue of canceled orders on Robinhood/real, I don't know what's up with that.

But it really doesn't take a whole lot to get it to a "good enough" state.

Yup. Meanwhile, if one looks up goodenough, one finds the inventor of Lithium-Ion batteries by that name, no joke. Some catch on fire (also no joke) like Samsung Note 7, however maybe one can think of them as still good enough. Heh. :P I have a Robinhood algo I think of as good enough to trade live/real. If we are fortunate, Quantopian will decide their own long-term good enough bar will be set high enough for few fires, maybe even higher, high enough to meet even my own, high, higher standards.

@ Dan -

I see that you are using:

# Create any factors we need
# latest_price is just used in case we don't have current price for an asset
latest_price = Factors.Latest(inputs =[USEquityPricing.close], mask=universe)
return Pipeline(
columns = {
'latest_price' : latest_price,
},
screen = universe,
)



So the 'latest_price' is the prior daily close, right? Shouldn't latest_price be the most recent minutely close?

I'm still thinking things though, but one problem with buying using a limit order with the current price is that theoretically, the order may never get filled, assuming that the stock price goes up monotonically during the day. One could actually estimate the probability of a fill based on the recent prior price statistics; my hunch is that it'll be pretty high, if the most recent minutely price is used, but if I'm understanding limit orders correctly, there would be no guarantee of a fill. If the algo is submitting orders serially, requiring a complete fill of one order before the next is submitted, this could really muck things up if an order hangs because of its limit price being too tight.

For Quantopian support, I'm wondering if the "Execution" step in the workflow includes a consideration of Robinhood trading? In another blog post, there is mention of "order/execution management system (O/EMS) at Quantopian"--I wonder if it will cover Robinhood? Or only the prime broker (IB?) that will handle the Q fund?

@Grant

Good catch. You are correct in noting the 'latest_price' factor is the prior day close and that what we really want is the current minute data. This price is always updated (ie over-written) to be the current minute price in the 'update_stock_data' function with the following code

# Get the latest prices for all our securities
# May want to account for possibility of price being NaN or 0?
output_df.latest_price = data.current(output_df.index, 'price')



That factor could be taken out of the pipeline definition. Just make sure you add 'latest_price' as a column in the 'before_trading_start'. Maybe something like this

# Add other columns to the dataframe for storing qty of shares held, etc
context.output = context.output.assign(
latest_price = 0.0
held_shares = 0,
target_shares = 0,
order_shares = 0,
target_value = 0.0,
order_value = 0.0,
weight = MY_ETFS.weight,
)


You will probably note that the pipeline isn't doing anything for us now (ie not fetching any data). It could be removed in the above algorithm example but it's handy to keep in place and used as an algorithm framework. One can then easily add factors and reference them in the code. For example one version of this I've tried adds a 'volatility' factor and uses that to adjust the weight (rather than a fixed weight).

As far as a limit order potentially not filling. Yes, that's a possibility and I've experienced it. Depending upon ones algorithm though this may not be a problem. You wondered if "this could really muck things up if an order hangs" . Not really, The logic in the above algorithm always checks available cash before posting any subsequent buys. The logic doesn't require a complete fill. If a limit order didn't fill then there simply won't be the associated available cash. Any buys then just get adjusted (and that limit order still remains until the end of day).

I've actually tried placing sell limit orders for a .5% above the current price and buy limit orders for .5% below the current price in the above algorithm. One could also try adjusting the price by perhaps the standard deviation or something more 'statistical'. This actually decreased beta and increased returns a bit. Again, depends upon ones algorithm though.

@ Dan -

I was thinking in terms of minute by minute submitting orders, and checking for fills one stock at a time, using limit orders, but I now realize that all can be submitted at once (although technically, they are separated in time, since Quantopian doesn't send orders in batches--they are sent as soon as they are encountered in the code, so there is an inherent sequence). The orders are independent, since the cash is distributed across them exactly with the limit orders; there is no need to place one order, wait for it to be filled, and then move on to the next. However, once could track fills, and re-attempt orders at a higher limit, if the original limit does not get hit in a certain amount of time.

@ Grant, This framework is inherently a trade once a day model. Figure out what you want to trade before the market opens and then place the trades. I've dabbled in trying to get the 'best' price during the day but never with much success. It's really just the same problem as trying to get the best price over the course of a longer period of time and I've never been able to crack that nut either. I'm sure others have, just not me.

You have a good idea. Maybe a simple start is to break the day into two. Put 'high' limits on orders initially, but then later in the day, cancel any unfilled orders and place more realistic ones. Perhaps base those 'realistic' orders on minutely trends up to that point? I have never tried to cancel an order through Robinhood. One would need to cancel, then wait for some indication that it really did cancel before placing a new order.

It sure seems like there should be something built in to Quantopian for Robinhood trading. I wonder if quantopian.experimental is available for user-contributed stuff like this? It seems like the right way to go would be to get something up on github, and make it available via quantopian.experimental. Then users could dink around with it until it is ready for prime time.

Is live trading on Robinhood still a thing?

Thanks,

Dan

Not directly on Quantopian, since brokerage integrations were discontinued.

Thanks for the response.

Is there any other place to algorithmically trade on Robinhood?

Not sure. Maybe someone here will provide guidance.

Is IB and Robinhood discontinued?

you can trade IB via zipline-live

alpaca markets goes live in Sep. https://blog.alpaca.markets/
They have a zipline custom for alpaca:
https://github.com/alpacahq/alpaca-zipline