Certain price values are not respected due to floating point rounding errors and truncation
JordanMandel opened this issue · 2 comments
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
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?
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.