diff --git a/src/analytics/math_scaler.py b/src/analytics/math_scaler.py index fc84d872..0784b8a8 100644 --- a/src/analytics/math_scaler.py +++ b/src/analytics/math_scaler.py @@ -2,6 +2,8 @@ from decimal import ROUND_DOWN, Decimal, InvalidOperation from typing import Union +from fractions import Fraction + # --------------------------------------------------------------------------- # Scale constants @@ -20,7 +22,6 @@ Number = Union[int, float, Decimal] - # --------------------------------------------------------------------------- # Internal helpers # --------------------------------------------------------------------------- @@ -160,30 +161,89 @@ def pack_rate(value: Number) -> int: """Convenience wrapper: scale *value* to a ``SCALE_7`` integer for payload packing. This is the canonical entry-point used before serialising a rate into any - Soroban contract data payload. It enforces the ``10^7`` fixed-integer base + Soroban contract data payload. It enforces the ``10^7`` fixed-integer base contract and rejects non-finite or boolean inputs early. + """ + return scale_up(value, SCALE_7) - Parameters - ---------- - value: - Raw exchange rate (int, float, or Decimal). - Returns - ------- - int - Deterministic ``SCALE_7`` integer ready for transmission. +ConversionMatrix = dict[str, dict[str, Fraction]] + + +def build_conversion_matrix( + rates: dict[tuple[str, str], Number], +) -> ConversionMatrix: """ - return scale_up(value, SCALE_7) + Build an exact fraction-based conversion matrix. + """ + matrix: ConversionMatrix = {} + + for (source, target), rate in rates.items(): + matrix.setdefault(source, {}) + + decimal_rate = _to_decimal(rate) + matrix[source][target] = Fraction(decimal_rate) + + return matrix + + +def convert_path( + matrix: ConversionMatrix, + path: list[str], +) -> Fraction: + """ + Compute an exact conversion along a multi-hop path. + """ + if len(path) < 2: + return Fraction(1) + + result = Fraction(1) + + for src, dst in zip(path, path[1:]): + try: + result *= matrix[src][dst] + except KeyError as exc: + raise KeyError(f"Missing conversion rate: {src} -> {dst}") from exc + + return result + + +def fraction_to_scaled( + value: Fraction, + factor: int = SCALE_7, +) -> int: + """ + Convert an exact Fraction into a SCALE_7 integer. + """ + return scale_up( + Decimal(value.numerator) / Decimal(value.denominator), + factor, + ) + + +def scaled_to_fraction( + value: int, + factor: int = SCALE_7, +) -> Fraction: + """ + Convert a SCALE_7 integer into an exact Fraction. + """ + return Fraction(value, factor) __all__ = [ "SCALE_7", "SCALE_14", "Number", + "ConversionMatrix", "scale_up", "scale_down", "multiply_rates", "cross_feed_multiply", "floor_divide", "pack_rate", -] + "build_conversion_matrix", + "convert_path", + "fraction_to_scaled", + "scaled_to_fraction", +] \ No newline at end of file