alexgolec / tda-api

A TD Ameritrade API client for Python. Includes historical data for equities and ETFs, options chains, streaming order book data, complex order construction, and more.
https://tda-api.readthedocs.io
MIT License
1.26k stars 338 forks source link

Certain price values are not respected due to floating point rounding errors and truncation #320

Open JordanMandel opened 2 years ago

JordanMandel commented 2 years ago

Description of Bug Certain price values (such as 5.06) are not respected when creating a limit order

Code to Reproduce

from tda.orders.equities import equity_buy_limit
order = equity_buy_limit('AAPL', quantity=100, price=5.06)
print(order.build())

Expected Behavior The resulting order should have a limit price of 5.06

Actual Behavior Instead, the limit order has a price of 5.05

{'session': 'NORMAL', 'duration': 'DAY', 'orderType': 'LIMIT', 'price': '5.05', 'orderLegCollection': [{'instruction': 'BUY', 'instrument': {'assetType': 'EQUITY', 'symbol': 'AAPL'}, 'quantity': 100}], 'orderStrategyType': 'SINGLE'}

The error is in truncate_float due to floating point rounding errors.

flt = 5.06
values = {
    'flt':                                            flt,
    'flt * 100':                                      flt * 100,
    'int(flt * 100)':                                 int(flt * 100),
    'float(int(flt * 100))':                          float(int(flt * 100)),
    'float(int(flt * 100)) / 100.0':                  float(int(flt * 100)) / 100.0,
    "'{:.2f}'.format(float(int(flt * 100)) / 100.0)": '{:.2f}'.format(float(int(flt * 100)) / 100.0),
}

for key, value in values.items():
    print(f'{key:<50}: {value}')
flt                                               : 5.06
flt * 100                                         : 505.99999999999994
int(flt * 100)                                    : 505
float(int(flt * 100))                             : 505.0
float(int(flt * 100)) / 100.0                     : 5.05
'{:.2f}'.format(float(int(flt * 100)) / 100.0)    : 5.05

Notice how flt * 100 is 505.99999999999994. A possible fix might be to add a small epsilon value (haven't tested negative values though):

def truncate_float_fixed(flt):
    epsilon = 1e-7
    if abs(flt) < 1 and flt != 0.0:
        return '{:.4f}'.format(float(int((flt + epsilon) * 10000)) / 10000.0)
    else:
        return '{:.2f}'.format(float(int((flt + epsilon) * 100)) / 100.0)

print('BEFORE: ', truncate_float(5.06))
print('AFTER : ', truncate_float_fixed(5.06))
BEFORE:  5.05
AFTER :  5.06
john157157 commented 2 years ago

I've been looking at the truncation issue too. JordanMandel - while your approach looks good to me, why not bypass the whole issue and pass the price as a string? Oh wait, the docs say

You can sidestep this entire process by passing your price as a string, although be forewarned that TDAmeritrade may reject your order or even interpret it in unexpected ways.

Does anyone know the circumstances where the TDAmeritrade weirdness happens? Fetched quotes routinely arrive with 4 decimals places of precision - eg 77.1234. Would placing a buy_limit order using the string "77.1234" ever give unexpected results?

tirthb commented 1 year ago

truncate_float(price) is not working 4.1 is giving "4.09"

https://github.com/alexgolec/tda-api/blob/master/tda/orders/generic.py#L35

I am passing the price as string for now.