Skip to content

Interest Rates

The InterestRate class eliminates confusion between decimal and percentage representations while providing safe conversions between different compounding frequencies.

The Problem with Raw Numbers

Interest rates are confusing when represented as raw numbers:

# Is this 5% or 500%? ðŸĪ”
rate = 0.05

# Is this 5% or 0.05%? ðŸĪ”  
rate = 5

# MoneyWarp makes it explicit
from money_warp import InterestRate

rate = InterestRate("5% a")  # Clearly 5% annually ✅

Creating Interest Rates

Multiple clear formats supported:

from money_warp import InterestRate, CompoundingFrequency

# String format (recommended)
annual = InterestRate("5.25% a")        # 5.25% annually
monthly = InterestRate("0.4375% m")     # 0.4375% monthly  
daily = InterestRate("0.0144% d")       # 0.0144% daily
quarterly = InterestRate("1.3125% q")   # 1.3125% quarterly

# Long form strings
annual_long = InterestRate("5.25% annual")
monthly_long = InterestRate("0.4375% monthly")

# Abbreviated notation (Brazilian/LatAm convention)
annual_abbrev = InterestRate("5.25% a.a.")    # ao ano
monthly_abbrev = InterestRate("0.4375% a.m.") # ao mÊs
daily_abbrev = InterestRate("0.0144% a.d.")   # ao dia
quarterly_abbrev = InterestRate("2.75% a.t.")  # ao trimestre
semi_annual_abbrev = InterestRate("3% a.s.")   # ao semestre

# Decimal format (explicit)
decimal_annual = InterestRate("0.0525 a")     # 5.25% as decimal
decimal_monthly = InterestRate("0.004375 m")  # 0.4375% as decimal

# Numeric with explicit frequency
numeric_rate = InterestRate(5.25, CompoundingFrequency.ANNUALLY, as_percentage=True)

print(f"Annual: {annual}")
print(f"Monthly: {monthly}")
print(f"Daily: {daily}")
print(f"Quarterly: {quarterly}")

Output:

Annual: 5.250% annually
Monthly: 0.438% monthly
Daily: 0.014% daily
Quarterly: 1.313% quarterly

Accessing Rate Values

Safe access to decimal and percentage representations:

rate = InterestRate("6.5% a")

print(f"As percentage: {rate.as_percentage()}")  # 6.5
print(f"As decimal: {rate.as_decimal()()}")        # 0.065
print(f"As float: {rate.as_float(4)}")           # 0.065
print(f"Frequency: {rate.period}")             # CompoundingFrequency.ANNUALLY
print(f"Display: {rate}")                      # 6.500% annually

Frequency Conversions

Convert between different compounding frequencies:

# Start with annual rate
annual = InterestRate("6% a")

# Convert to other frequencies
monthly = annual.to_monthly()
daily = annual.to_daily()
quarterly = annual.to_quarterly()

print(f"Annual: {annual}")
print(f"Monthly equivalent: {monthly}")
print(f"Daily equivalent: {daily}")
print(f"Quarterly equivalent: {quarterly}")

Output:

Annual: 6.000% annually
Monthly equivalent: 0.487% monthly
Daily equivalent: 0.016% daily
Quarterly equivalent: 1.467% quarterly

Understanding Effective vs Nominal Rates

MoneyWarp handles the conversion between nominal and effective rates:

# Nominal 12% annual rate
nominal = InterestRate("12% a")

# What's the effective monthly rate?
monthly_effective = nominal.to_monthly()
print(f"Nominal annual: {nominal}")
print(f"Effective monthly: {monthly_effective}")

# Verify: 12 months of monthly rate should equal annual
annual_check = (1 + monthly_effective.as_decimal()) ** 12 - 1
print(f"Verification: {annual_check:.6f} ≈ {nominal.as_decimal():.6f}")

Output:

Nominal annual: 12.000% annually
Effective monthly: 0.949% monthly
Verification: 0.120000 ≈ 0.120000

Real-World Example: Credit Card APR

def analyze_credit_card(balance, apr_string, monthly_payment):
    """Analyze credit card payoff with daily compounding."""

    from money_warp import Money

    balance = Money(balance)
    monthly_payment = Money(monthly_payment)

    # Credit cards typically compound daily
    apr = InterestRate(apr_string)
    daily_rate = apr.to_daily()

    print(f"Credit Card Analysis:")
    print(f"Balance: {balance}")
    print(f"APR: {apr}")
    print(f"Daily rate: {daily_rate}")
    print(f"Monthly payment: {monthly_payment}")
    print()

    # Calculate monthly interest (30 days)
    monthly_interest = balance * (daily_rate.as_decimal() * 30)
    principal_payment = monthly_payment - monthly_interest

    print(f"Monthly breakdown:")
    print(f"  Interest (30 days): {monthly_interest}")
    print(f"  Principal: {principal_payment}")

    if principal_payment <= Money.zero():
        print("⚠ïļ  Payment doesn't cover interest!")
        return

    # Estimate payoff time (simplified)
    months = 0
    current_balance = balance

    while current_balance > Money.zero() and months < 600:  # Max 50 years
        interest = current_balance * (daily_rate.as_decimal() * 30)
        principal = monthly_payment - interest

        if principal <= Money.zero():
            break

        current_balance -= principal
        months += 1

        if current_balance < Money.zero():
            current_balance = Money.zero()

    print(f"Estimated payoff: {months} months ({months/12:.1f} years)")
    total_paid = monthly_payment * months
    total_interest = total_paid - balance
    print(f"Total paid: {total_paid}")
    print(f"Total interest: {total_interest}")

# Analyze a typical credit card scenario
analyze_credit_card("5000.00", "18.99% a", "150.00")

Output:

Credit Card Analysis:
Balance: 5,000.00
APR: 18.990% annually
Daily rate: 0.047% daily
Monthly payment: 150.00

Monthly breakdown:
  Interest (30 days): 78.95
  Principal: 71.05

Estimated payoff: 48 months (4.0 years)
Total paid: 7,200.00
Total interest: 2,200.00

Mortgage Rate Comparisons

def compare_mortgage_rates(principal, loan_term_years, rates):
    """Compare different mortgage rates."""

    from money_warp import Money, Loan
    from datetime import date, timedelta

    principal = Money(principal)

    print(f"Mortgage Comparison - {principal} over {loan_term_years} years")
    print("=" * 60)

    # Generate monthly payment schedule
    start_date = date(2024, 1, 1)
    num_payments = loan_term_years * 12
    due_dates = [start_date + timedelta(days=30*i) for i in range(1, num_payments + 1)]

    results = []

    for rate_str in rates:
        rate = InterestRate(rate_str)
        loan = Loan(principal, rate, due_dates)
        schedule = loan.get_amortization_schedule()

        monthly_payment = schedule[0].payment_amount
        total_interest = schedule.total_interest
        total_paid = schedule.total_payments

        results.append({
            'rate': rate,
            'monthly_payment': monthly_payment,
            'total_interest': total_interest,
            'total_paid': total_paid
        })

        print(f"{rate_str:>8} | {monthly_payment:>10} | {total_interest:>12} | {total_paid:>12}")

    # Find best rate
    best = min(results, key=lambda x: x['total_interest'])
    worst = max(results, key=lambda x: x['total_interest'])
    savings = worst['total_interest'] - best['total_interest']

    print("=" * 60)
    print(f"Best rate: {best['rate']} saves {savings} vs worst rate")

# Compare common mortgage rates
rates = ["3.5% a", "4.0% a", "4.5% a", "5.0% a", "5.5% a"]
compare_mortgage_rates("300000.00", 30, rates)

High-Frequency Trading Example

def calculate_compound_returns(principal, daily_rate_pct, days):
    """Calculate returns with daily compounding."""

    from money_warp import Money

    principal = Money(principal)
    daily_rate = InterestRate(f"{daily_rate_pct}% d")

    print(f"Compound Growth Analysis:")
    print(f"Principal: {principal}")
    print(f"Daily rate: {daily_rate}")
    print(f"Period: {days} days")
    print()

    # Calculate compound growth
    growth_factor = (1 + daily_rate.as_decimal()) ** days
    final_amount = principal * growth_factor
    total_return = final_amount - principal

    # Convert to annual equivalent
    annual_equivalent = InterestRate(
        float((growth_factor ** (365/days)) - 1), 
        CompoundingFrequency.ANNUALLY, 
        as_percentage=False
    )

    print(f"Final amount: {final_amount}")
    print(f"Total return: {total_return}")
    print(f"Return percentage: {(total_return / principal) * 100:.2f}%")
    print(f"Annualized rate: {annual_equivalent}")

# Example: 0.1% daily return over 100 days
calculate_compound_returns("10000.00", "0.1", 100)

Custom Periodic Rates

# For non-standard periods
rate = InterestRate("8% a")

# Convert to any period (e.g., weekly = 52 periods per year)
weekly_rate = rate.to_periodic_rate(52)
print(f"Weekly rate: {weekly_rate:.6f}")

# Bi-weekly (26 periods per year)
biweekly_rate = rate.to_periodic_rate(26)
print(f"Bi-weekly rate: {biweekly_rate:.6f}")

# Custom: every 45 days (365/45 ≈ 8.11 periods per year)
custom_periods = 365 / 45
custom_rate = rate.to_periodic_rate(custom_periods)
print(f"45-day rate: {custom_rate:.6f}")

Rate Validation and Error Handling

try:
    # Invalid format
    bad_rate = InterestRate("5% x")  # 'x' is not a valid frequency
except ValueError as e:
    print(f"Error: {e}")

try:
    # Missing frequency for numeric input
    bad_rate = InterestRate(0.05)  # No frequency specified
except ValueError as e:
    print(f"Error: {e}")

# Valid alternatives
good_rate1 = InterestRate(0.05, CompoundingFrequency.ANNUALLY, as_percentage=False)
good_rate2 = InterestRate(5, CompoundingFrequency.ANNUALLY, as_percentage=True)
good_rate3 = InterestRate("5% a")  # Recommended

print(f"All equivalent: {good_rate1} = {good_rate2} = {good_rate3}")

Abbreviated Notation (str_style)

MoneyWarp supports abbreviated period labels commonly used in Brazilian and Latin American finance. The str_style parameter controls how __str__ renders the period:

Frequency Long ("long") Abbreviated ("abbrev")
Annually 5.250% annually 5.250% a.a.
Monthly 0.500% monthly 0.500% a.m.
Daily 0.014% daily 0.014% a.d.
Quarterly 2.750% quarterly 2.750% a.t.
Semi-annually 3.000% semi_annually 3.000% a.s.

Auto-detection from string input

When you parse a string that uses abbreviated tokens, the style is set automatically:

rate = InterestRate("5.25% a.a.")
print(rate)  # "5.250% a.a." — round-trips without extra config

Explicit style on numeric rates

For rates created from numbers, pass str_style="abbrev":

rate = InterestRate(
    1.5, CompoundingFrequency.MONTHLY,
    as_percentage=True, str_style="abbrev",
)
print(rate)  # "1.500% a.m."

Style propagates through conversions

Converting a rate preserves its display style:

annual = InterestRate("6% a.a.")
monthly = annual.to_monthly()
daily = annual.to_daily()

print(annual)   # "6.000% a.a."
print(monthly)  # "0.487% a.m."
print(daily)    # "0.016% a.d."

Configurable Display Formatting

You can control the number of decimal places and the abbreviation labels used by __str__:

Decimal places (str_decimals)

# Default: 3 decimal places
rate = InterestRate("3.99% a.m.")
print(rate)  # "3.990% a.m."

# 2 decimal places
rate = InterestRate("3.99% a.m.", str_decimals=2)
print(rate)  # "3.99% a.m."

# 0 decimal places
rate = InterestRate("3.99% a.m.", str_decimals=0)
print(rate)  # "4% a.m."

Custom abbreviation labels (abbrev_labels)

Override any subset of the default abbreviation map. Unspecified keys keep their defaults:

from money_warp import InterestRate, CompoundingFrequency

# Drop the trailing dot on monthly only
rate = InterestRate(
    "3.99% a.m.",
    abbrev_labels={CompoundingFrequency.MONTHLY: "a.m"},
)
print(rate)  # "3.990% a.m"

# Combine both for the old-style output: "3.99% a.m"
rate = InterestRate(
    "3.99% a.m.",
    str_decimals=2,
    abbrev_labels={CompoundingFrequency.MONTHLY: "a.m"},
)
print(rate)  # "3.99% a.m"

# Override all abbreviations at once
no_dots = {
    CompoundingFrequency.ANNUALLY: "a.a",
    CompoundingFrequency.MONTHLY: "a.m",
    CompoundingFrequency.DAILY: "a.d",
    CompoundingFrequency.QUARTERLY: "a.t",
    CompoundingFrequency.SEMI_ANNUALLY: "a.s",
}
rate = InterestRate("5.25% a.a.", abbrev_labels=no_dots)
print(rate)  # "5.250% a.a"

Display settings propagate through conversions

labels = {CompoundingFrequency.MONTHLY: "a.m", CompoundingFrequency.ANNUALLY: "a.a"}
rate = InterestRate("12% a.a.", str_decimals=2, abbrev_labels=labels)

print(rate)             # "12.00% a.a"
print(rate.to_monthly())  # "0.95% a.m"

Year Size (Day-Count Convention)

Financial markets use different day-count conventions when converting between daily and annual rates. MoneyWarp supports two conventions through the YearSize enum:

Convention Days Typical use
YearSize.commercial 365 Calendar-day markets (default)
YearSize.banker 360 International banking, some LatAm conventions

Basic usage

from money_warp import InterestRate, YearSize

# Default: commercial (365 days)
commercial = InterestRate("10% a")
print(commercial.to_daily())  # 365th root of 1.10

# Banker convention (360 days)
banker = InterestRate("10% a", year_size=YearSize.banker)
print(banker.to_daily())  # 360th root of 1.10 — slightly higher daily rate

How year size affects calculations

The same annual rate produces different daily rates depending on the convention:

from money_warp import InterestRate, YearSize, Money

rate_365 = InterestRate("10% a", year_size=YearSize.commercial)
rate_360 = InterestRate("10% a", year_size=YearSize.banker)

print(f"Commercial daily: {rate_365.to_daily().as_decimal():.10f}")  # ~0.0002611578
print(f"Banker daily:     {rate_360.to_daily().as_decimal():.10f}")  # ~0.0002651568

# This difference compounds when accruing interest
principal = Money("100000")
print(f"30-day accrual (365): {rate_365.accrue(principal, 30)}")
print(f"30-day accrual (360): {rate_360.accrue(principal, 30)}")  # higher

Year size propagates through conversions

Once set, the year size carries through all frequency conversions:

rate = InterestRate("10% a", year_size=YearSize.banker)

daily = rate.to_daily()
monthly = rate.to_monthly()
back_to_annual = daily.to_annual()

print(daily.year_size)           # YearSize.banker
print(monthly.year_size)         # YearSize.banker
print(back_to_annual.year_size)  # YearSize.banker

Inspecting year size

rate = InterestRate("5% a", year_size=YearSize.banker)

print(rate.year_size)       # YearSize.banker
print(rate.year_size.value) # 360
print(repr(rate))           # includes year_size when non-default

Best Practices

  1. Use string format: InterestRate("5.25% a") is clearest
  2. Be explicit: Always specify frequency (a/m/d/q/s or a.a./a.m./a.d./a.t./a.s.)
  3. Convert appropriately: Match compounding to your calculation needs
  4. Validate inputs: Handle user input with try/catch
  5. Document assumptions: Make compounding frequency clear in your code
  6. Use abbreviated notation when integrating with Brazilian/LatAm financial systems
  7. Set year_size when your market uses the 360-day banker convention

Common Patterns

# Reading rates from configuration
config_rates = {
    'savings': "0.5% a",
    'checking': "0.01% a", 
    'mortgage': "4.25% a",
    'credit_card': "18.99% a"
}

rates = {name: InterestRate(rate_str) for name, rate_str in config_rates.items()}

# Comparing rates (convert to same frequency)
annual_rates = {name: rate.to_annual() for name, rate in rates.items()}
for name, rate in annual_rates.items():
    print(f"{name}: {rate}")

# Finding the best rate
best_savings = max(rates['savings'], rates['checking'], key=lambda r: r.as_decimal())
print(f"Best savings rate: {best_savings}")

Interest rates are now type-safe and crystal clear! 📈