def get_balance_tolerance(balance_entry, options_map): """Get the tolerance amount for a single entry. Args: balance_entry: An instance of data.Balance options_map: An options dict, as per the parser. Returns: A Decimal, the amount of tolerance implied by the directive. """ if balance_entry.tolerance is not None: # Use the balance-specific tolerance override if it is provided. tolerance = balance_entry.tolerance else: expo = balance_entry.amount.number.as_tuple().exponent if expo < 0: # Be generous and always allow twice the multiplier on Balance and # Pad because the user creates these and the rounding of those # balances may often be further off than those used within a single # transaction. tolerance = options_map["inferred_tolerance_multiplier"] * 2 tolerance = ONE.scaleb(expo) * tolerance else: tolerance = ZERO return tolerance
def get_tolerance(balance_entry, options_map): """Get the tolerance amount for a single entry. Args: balance_entry: An instance of data.Balance options_map: An options dict, as per the parser. Returns: A Decimal, the amount of tolerance implied by the directive. """ if options_map['use_legacy_fixed_tolerances']: # This is to support the legacy behavior to ease the transition # for some users. tolerance = D('0.015') elif (options_map["experiment_explicit_tolerances"] and balance_entry.tolerance is not None): # Use the balance-specific tolerance override if it is provided. tolerance = balance_entry.tolerance else: expo = balance_entry.amount.number.as_tuple().exponent if expo < 0: # Be generous and always allow twice the multiplier on Balance and # Pad because the user creates these and the rounding of those # balances may often be further off than those used within a single # transaction. tolerance = options_map["inferred_tolerance_multiplier"] * 2 tolerance = ONE.scaleb(expo) * tolerance else: tolerance = ZERO return tolerance
def infer_tolerances(postings, options_map, use_cost=None): """Infer tolerances from a list of postings. The tolerance is the maximum fraction that is being used for each currency (a dict). We use the currency of the weight amount in order to infer the quantization precision for each currency. Integer amounts aren't contributing to the determination of precision. The 'use_cost' option allows one to experiment with letting postings at cost and at price influence the maximum value of the tolerance. It's tricky to use and alters the definition of the tolerance in a non-trivial way, if you use it. The tolerance is expanded by the sum of the cost times a fraction 'M' of the smallest digits in the number of units for all postings held at cost. For example, in this transaction: 2006-01-17 * "Plan Contribution" Assets:Investments:VWELX 18.572 VWELX {30.96 USD} Assets:Investments:VWELX 18.572 VWELX {30.96 USD} Assets:Investments:Cash -1150.00 USD The tolerance for units of USD will calculated as the MAXIMUM of: 0.01 * M = 0.005 (from the 1150.00 USD leg) The sum of 0.001 * M x 30.96 = 0.01548 + 0.001 * M x 30.96 = 0.01548 = 0.03096 So the tolerance for USD in this case is max(0.005, 0.03096) = 0.03096. Prices contribute similarly to the maximum tolerance allowed. Note that 'M' above is the inferred_tolerance_multiplier and its default value is 0.5. Args: postings: A list of Posting instances. options_map: A dict of options. use_cost: A boolean, true if we should be using a combination of the smallest digit of the number times the cost or price in order to infer the tolerance. If the value is left unspecified (as 'None'), the default value can be overridden by setting an option. Returns: A dict of currency to the tolerated difference amount to be used for it, e.g. 0.005. """ if use_cost is None: use_cost = options_map["infer_tolerance_from_cost"] inferred_tolerance_multiplier = options_map["inferred_tolerance_multiplier"] default_tolerances = options_map["inferred_tolerance_default"] tolerances = default_tolerances.copy() cost_tolerances = collections.defaultdict(D) for posting in postings: # Skip the precision on automatically inferred postings. if posting.meta and AUTOMATIC_META in posting.meta: continue units = posting.units if not (isinstance(units, Amount) and isinstance(units.number, Decimal)): continue # Compute bounds on the number. currency = units.currency expo = units.number.as_tuple().exponent if expo < 0: # Note: the exponent is a negative value. tolerance = ONE.scaleb(expo) * inferred_tolerance_multiplier tolerances[currency] = max(tolerance, tolerances.get(currency, -1024)) if not use_cost: continue # Compute bounds on the smallest digit of the number implied as cost. cost = posting.cost if cost is not None: cost_currency = cost.currency if isinstance(cost, Cost): cost_tolerance = min(tolerance * cost.number, MAXIMUM_TOLERANCE) else: assert isinstance(cost, CostSpec) cost_tolerance = MAXIMUM_TOLERANCE for cost_number in cost.number_total, cost.number_per: if cost_number is None or cost_number is MISSING: continue cost_tolerance = min(tolerance * cost_number, cost_tolerance) cost_tolerances[cost_currency] += cost_tolerance # Compute bounds on the smallest digit of the number implied as cost. price = posting.price if isinstance(price, Amount) and isinstance(price.number, Decimal): price_currency = price.currency price_tolerance = min(tolerance * price.number, MAXIMUM_TOLERANCE) cost_tolerances[price_currency] += price_tolerance for currency, tolerance in cost_tolerances.items(): tolerances[currency] = max(tolerance, tolerances.get(currency, -1024)) default = tolerances.pop('*', ZERO) return defdict.ImmutableDictWithDefault(tolerances, default=default)