Files
ccxt_with_mt5/ccxt/base/decimal_to_precision.py
lz_db 0fab423a18 add
2025-11-16 12:31:03 +08:00

179 lines
6.6 KiB
Python

import decimal
import numbers
import itertools
import re
__all__ = [
'TRUNCATE',
'ROUND',
'ROUND_UP',
'ROUND_DOWN',
'DECIMAL_PLACES',
'SIGNIFICANT_DIGITS',
'TICK_SIZE',
'NO_PADDING',
'PAD_WITH_ZERO',
'decimal_to_precision',
]
# rounding mode
TRUNCATE = 0
ROUND = 1
ROUND_UP = 2
ROUND_DOWN = 3
# digits counting mode
DECIMAL_PLACES = 2
SIGNIFICANT_DIGITS = 3
TICK_SIZE = 4
# padding mode
NO_PADDING = 5
PAD_WITH_ZERO = 6
def decimal_to_precision(n, rounding_mode=ROUND, precision=None, counting_mode=DECIMAL_PLACES, padding_mode=NO_PADDING):
assert precision is not None, 'precision should not be None'
if isinstance(precision, str):
precision = float(precision)
assert isinstance(precision, float) or isinstance(precision, decimal.Decimal) or isinstance(precision, numbers.Integral), 'precision has an invalid number'
if counting_mode == TICK_SIZE:
assert precision > 0, 'negative or zero precision can not be used with TICK_SIZE precisionMode'
else:
assert isinstance(precision, numbers.Integral)
assert rounding_mode in [TRUNCATE, ROUND]
assert counting_mode in [DECIMAL_PLACES, SIGNIFICANT_DIGITS, TICK_SIZE]
assert padding_mode in [NO_PADDING, PAD_WITH_ZERO]
# end of checks
context = decimal.getcontext()
if counting_mode != TICK_SIZE:
precision = min(context.prec - 2, precision)
# all default except decimal.Underflow (raised when a number is rounded to zero)
context.traps[decimal.Underflow] = True
context.rounding = decimal.ROUND_HALF_UP # rounds 0.5 away from zero
dec = decimal.Decimal(str(n))
precision_dec = decimal.Decimal(str(precision))
string = '{:f}'.format(dec) # convert to string using .format to avoid engineering notation
precise = None
def power_of_10(x):
return decimal.Decimal('10') ** (-x)
if precision < 0:
if counting_mode == TICK_SIZE:
raise ValueError('TICK_SIZE cant be used with negative numPrecisionDigits')
to_nearest = power_of_10(precision)
if rounding_mode == ROUND:
return "{:f}".format(to_nearest * decimal.Decimal(decimal_to_precision(dec / to_nearest, rounding_mode, 0, DECIMAL_PLACES, padding_mode)))
elif rounding_mode == TRUNCATE:
return decimal_to_precision(dec - dec % to_nearest, rounding_mode, 0, DECIMAL_PLACES, padding_mode)
if counting_mode == TICK_SIZE:
# python modulo with negative numbers behaves different than js/php, so use abs first
missing = abs(dec) % precision_dec
if missing != 0:
if rounding_mode == ROUND:
if dec > 0:
if missing >= precision_dec / 2:
dec = dec - missing + precision_dec
else:
dec = dec - missing
else:
if missing >= precision_dec / 2:
dec = dec + missing - precision_dec
else:
dec = dec + missing
elif rounding_mode == TRUNCATE:
if dec < 0:
dec = dec + missing
else:
dec = dec - missing
parts = re.sub(r'0+$', '', '{:f}'.format(precision_dec)).split('.')
if len(parts) > 1:
new_precision = len(parts[1])
else:
match = re.search(r'0+$', parts[0])
if match is None:
new_precision = 0
else:
new_precision = - len(match.group(0))
return decimal_to_precision('{:f}'.format(dec), ROUND, new_precision, DECIMAL_PLACES, padding_mode)
if rounding_mode == ROUND:
if counting_mode == DECIMAL_PLACES:
precise = '{:f}'.format(dec.quantize(power_of_10(precision))) # ROUND_HALF_EVEN is default context
elif counting_mode == SIGNIFICANT_DIGITS:
q = precision - dec.adjusted() - 1
sigfig = power_of_10(q)
if q < 0:
string_to_precision = string[:precision]
# string_to_precision is '' when we have zero precision
below = sigfig * decimal.Decimal(string_to_precision if string_to_precision else '0')
above = below + sigfig
precise = '{:f}'.format(min((below, above), key=lambda x: abs(x - dec)))
else:
precise = '{:f}'.format(dec.quantize(sigfig))
if precise.startswith('-0') and all(c in '0.' for c in precise[1:]):
precise = precise[1:]
elif rounding_mode == TRUNCATE:
# Slice a string
if counting_mode == DECIMAL_PLACES:
before, after = string.split('.') if '.' in string else (string, '')
precise = before + '.' + after[:precision]
elif counting_mode == SIGNIFICANT_DIGITS:
if precision == 0:
return '0'
dot = string.index('.') if '.' in string else len(string)
start = dot - dec.adjusted()
end = start + precision
# need to clarify these conditionals
if dot >= end:
end -= 1
if precision >= len(string.replace('.', '')):
precise = string
else:
precise = string[:end].ljust(dot, '0')
if precise.startswith('-0') and all(c in '0.' for c in precise[1:]):
precise = precise[1:]
precise = precise.rstrip('.')
if padding_mode == NO_PADDING:
return precise.rstrip('0').rstrip('.') if '.' in precise else precise
elif padding_mode == PAD_WITH_ZERO:
if '.' in precise:
if counting_mode == DECIMAL_PLACES:
before, after = precise.split('.')
return before + '.' + after.ljust(precision, '0')
elif counting_mode == SIGNIFICANT_DIGITS:
fsfg = len(list(itertools.takewhile(lambda x: x == '.' or x == '0', precise)))
if '.' in precise[fsfg:]:
precision += 1
return precise[:fsfg] + precise[fsfg:].rstrip('0').ljust(precision, '0')
else:
if counting_mode == SIGNIFICANT_DIGITS:
if precision > len(precise):
return precise + '.' + (precision - len(precise)) * '0'
elif counting_mode == DECIMAL_PLACES:
if precision > 0:
return precise + '.' + precision * '0'
return precise
def number_to_string(x):
# avoids scientific notation for too large and too small numbers
if x is None:
return None
d = decimal.Decimal(str(x))
formatted = '{:f}'.format(d)
return formatted.rstrip('0').rstrip('.') if '.' in formatted else formatted