jmfernandes / robin_stocks

This is a library to use with Robinhood Financial App. It currently supports trading crypto-currencies, options, and stocks. In addition, it can be used to get real time ticker information, assess the performance of your portfolio, and can also get tax documents, total dividends paid, and more. More info at
http://www.robin-stocks.com
MIT License
1.68k stars 457 forks source link

DOGE buying and selling #220

Open suppleej opened 3 years ago

suppleej commented 3 years ago

Error: {'quantity': ['Order quantity has invalid increment.']}

rs.orders.order_buy_crypto_by_price(symbol, amount, priceType='mark_price', timeInForce='gtc')

Where symbol = 'DOGE', amount is a float 12.74

Other symbols like ETH and BTC work fine, but DOGE always gives an error. Other data pulls work for DOGE like rs.crypto.get_crypto_historicals(coinType, interval='15second', span='hour', bounds='24_7') but appears to just be an issue with the buy and sell orders.

jmfernandes commented 3 years ago

yes, it's a rounding issues since DOGE coin is such a small price. I thought I fixed it once, but I still need to play around with the rounding algorithm. The issue is I can't pass in too many digits or too little, but because robinhood is a private API I have no idea of knowing what the acceptable range is besides trial and error.

suppleej commented 3 years ago

Gotcha, that helps, I'll look around and see if I find anything as well, thanks!

nkhalsa commented 3 years ago

I had this issue too with DOGE and was playing around with the rounding algorithm. I ended up adding an optional argument forceWholeNumber.

`def round_price(price, forceWholeNumber=False): """Takes a price and rounds it to an appropriate decimal place that Robinhood will accept.

:param price: The input price to round.
:type price: float or int
:returns: The rounded price as a float.

"""
price = float(price)

if(forceWholeNumber):
    return round(price)

if price <= 1e-2:
    returnPrice = round(price, 6)
elif price < 1e0:
    returnPrice = round(price, 4)
else:
    returnPrice = round(price, 2)
return returnPrice

`

Then, added an optional argument to order_buy_crypto_by_price: ` @helper.login_required def order_buy_crypto_by_price(symbol, amountInDollars, priceType='ask_price', timeInForce='gtc', forceWholeShares=False): """Submits a market order for a crypto by specifying the amount in dollars that you want to trade. Good for share fractions up to 8 decimal places.

:param symbol: The crypto ticker of the crypto to trade.
:type symbol: str
:param amountInDollars: The amount in dollars of the crypto you want to buy.
:type amountInDollars: float
:param priceType: The type of price to get. Can be 'ask_price', 'bid_price', or 'mark_price'
:type priceType: str
:param timeInForce: Changes how long the order will be in effect for. 'gtc' = good until cancelled. \
'gfd' = good for the day. 'ioc' = immediate or cancel. 'opg' execute at opening.
:type timeInForce: Optional[str]
:returns: Dictionary that contains information regarding the buying of crypto, \
such as the order id, the state of order (queued, confired, filled, failed, canceled, etc.), \
the price, and the quantity.

""" 
try:
    symbol = symbol.upper().strip()
except AttributeError as message:
    print(message, file=helper.get_output())
    return None

crypto_info = crypto.get_crypto_info(symbol)
price = helper.round_price(crypto.get_crypto_quote_from_id(
    crypto_info['id'], info=priceType))
# turn the money amount into decimal number of shares
try:
    shares = helper.round_price(amountInDollars/price, forceWholeNumber=forceWholeShares)
except:
    shares = 0

payload = {
    'account_id': crypto.load_crypto_profile(info="id"),
    'currency_pair_id': crypto_info['id'],
    'price': price,
    'quantity': shares,
    'ref_id': str(uuid4()),
    'side': 'buy',
    'time_in_force': timeInForce,
    'type': 'market'
}

url = urls.order_crypto()
data = helper.request_post(url, payload, json=True)
return(data)

`

I can submit a pull request if this is a helpful solution

zabaat commented 3 years ago

@nkhalsa

I can submit a pull request if this is a helpful solution

I found your code useful :)

haildoge

@suppleej I can confirm the changes do work

r.order_buy_crypto_by_price('DOGE', 10, forceWholeShares=True)
{'account_id': 'xxx', 'average_price': None, 'cancel_url': 'https://nummus.robinhood.com/orders/xxx/cancel/', 'created_at': '2021-02-18T02:04:54.962825-05:00', 'cumulative_quantity': '0.000000000000000000', 'currency_pair_id': '1ef78e1b-049b-4f12-90e5-555dcf2fe204', 'executions': [], 'id': 'x', 'last_transaction_at': None, 'price': '0.051300000000000000', 'quantity': '195.000000000000000000', 'ref_id': '687142c1-80f3-46e3-9151-a4d395b68c60', 'rounded_executed_notional': '0.00', 'side': 'buy', 'state': 'unconfirmed', 'time_in_force': 'gtc', 'type': 'market', 'updated_at': '2021-02-18T02:04:55.325544-05:00'}
BigPines commented 3 years ago

Any chance we can get a fix for this? I wrote an app specifically to trade DOGE and this bug is a showstopper for me. :(

zabaat commented 3 years ago

Any chance we can get a fix for this? I wrote an app specifically to trade DOGE and this bug is a showstopper for me. :(

It does work if you make the changes, not ideal but no need to stop the show my dude.

thedogemustgoon

BigPines commented 3 years ago

Thanks. I made the changes above and am still trying to work through this. I am only placing limit orders so I am having to change things in other places too. I really hope I can make this work. I have "invested" a lot in writing this app only to be slapped down with this unexpected issue. :( Hoping for a proper fix sooner rather than later.

emwitchcraft commented 3 years ago

If you call get_crypto_positions () and look through the results, you'll see nested in the 'currency' key's value a dict with an 'increment' key. That value is the minimum quantity/coin increment for buy and sell orders through the api (oddly enough it's different on the app i.e. you can buy and sell fractional doge through it).

A couple share the same value, but otherwise they all have different minimum increments. I think the rounding method in the robin_stocks module worked in almost all cases simply by rounding off enough decimal places that the quantity ordered was always dividable by the minimum increment, except in the case of doge which has a minimum increment of 1.

I've been scootin my way around this for doge by dividing how much I want to buy or sell in usd by the current exchange price, rounding that result to an integer, then using one of the 'by quantity' variations of the ordering calls, rather than the 'by price' versions.

I suppose you could do that in the 'by price' options if you convert back from the rounded quantity to its equivalent usd.

mikedeskevich commented 3 years ago

Here's a snippet of what I've been doing and it seems to be working. I could use ceil or floor if I needed better guarantees, but for my case round is working just fine. The crypto_info dictionary is populated at the beginning of my program using the get_crypto_info api funciton.

    crypto_info[crypto] = rh.get_crypto_info(crypto)

    digits = -int(math.log10(float(crypto_info[crypto]['min_order_price_increment'])))
    bid_price[crypto] = round(float(quote['bid_price']), digits)
    ask_price[crypto] = round(float(quote['ask_price']), digits)

    digits = -int(math.log10(float(crypto_info[crypto]['min_order_quantity_increment'])))
    order_coins = min(round(delta_coins, digits), float(crypto_info[crypto]['max_order_size']))