May 4, 2020
Python Currency Converter
@anthonycorletti

Most internet services are also financial services. From recurring subscription payments, investment banking, issuing transfers and payouts to bank accounts, consumer retail, forex exchanges, bitcoin, payroll services; take your pick, there is plenty of opportunity to work with finance in tech.

Building these services properly and securely should not be a challenge. No one should end up with something like $100.000000000000000034 showing up on their bank statement.

In this post, I'm going to show you how to build a minimal financial service with the use of python, Babel.numbers, FastAPI, and Rates API.

To follow this post as written, using python 3.7+ is suggested.


To illustrate why it's never a good idea to use float point arithmetic for working with financial data in python, let's look at a simple example.

>>> float('0.1') + float('0.2')
0.30000000000000004

No bueno.

On paper, we'd say that 0.1 + 0.2 = 0.3. So now what? How do we get 0.3?!

>>> from decimal import Decimal
>>> r = Decimal('0.1') + Decimal('0.2')
Decimal('0.3')

Voila! We'll get to formatting the figures later.

Let's start by modeling out our system and its features.

Create the following files in a directory so that you have a project structure that looks like the following tree:

.
├── main.py
├── requirements.txt
└── schemas.py

Let's add everything we'll need to our requirements.txt

Babel==2.8.0
fastapi==0.47.1
pydantic==1.5.1
requests==2.22.0
uvicorn==0.11.2

Let's start writing main.py which will serve as our main router and point of function for this example. For larger apps, you should use fastapi's APIRouter module for defining your routes and a neat application structure for structuring the actions that take place in each route. Not to mention a database connection, I'm saving that for a later post.

from fastapi import FastAPI

from schemas import Payment

api = FastAPI()

@api.get('/health')
def health():
    return


@api.post('/payment', response_model=Payment, tags=['payment'])
def create_payment(payment: Payment):
    return

Nice, nothing fancy here. We're just going to be able to create a payment and see what was returned. It looks like we've defined some pydantic schemas in our main.py so let's write those.

Open up your schemas.py and drop this in.

from decimal import Decimal
from typing import Optional

import babel.numbers as bn
from pydantic import UUID4, BaseModel, validator

CURRENCIES = bn.list_currencies()
CURRENCY_VALUE_ERROR = ValueError(
    "Invalid currency code submitted. "
    f"Value must be one of the following {', '.join(CURRENCIES)}.")


class Payment(BaseModel):
    message: str
    sender_id: UUID4
    sender_currency: str
    sender_amount: Decimal
    sender_amount_formatted: Optional[str]
    receiver_id: UUID4
    receiver_currency: str
    receiver_amount_formatted: Optional[str]
    receiver_amount: Optional[Decimal]

    @validator('sender_amount')
    def positive_sender_amount(cls: BaseModel, v: Decimal) -> Decimal:
        if v <= 0:
            raise ValueError('Amount must be a positive number.')
        return v

    @validator('receiver_amount')
    def positive_receiver_amount(cls: BaseModel, v: Decimal) -> Decimal:
        if v and v <= 0:
            raise ValueError('Amount must be a positive number.')
        return v

    @validator('receiver_currency')
    def valid_receiver_currency(cls: BaseModel, v: str) -> str:
        if v not in CURRENCIES:
            raise CURRENCY_VALUE_ERROR
        return v

    @validator('sender_currency')
    def valid_sender_currency(cls: BaseModel, v: str) -> str:
        if v not in CURRENCIES:
            raise CURRENCY_VALUE_ERROR
        return v

Simple enough!

Awesome! With our schema written, we can build our currency converter in main.py.

from decimal import Decimal, getcontext

import babel.numbers as bn
import requests
from fastapi import FastAPI

from schemas import Payment

api = FastAPI()


def get_rate(sender_currency: str, receiver_currency: str) -> str:
    result = requests.get(
        "https://api.ratesapi.io/api/latest?"
        f"base={sender_currency}&symbols={receiver_currency}")
    return str(result.json().get('rates').get(receiver_currency))


def calculate_receiver_amount(payment: Payment) -> Decimal:
    rate = get_rate(payment.sender_currency, payment.receiver_currency)
    # we can set and alter decimal precision setting however we want
    # https://docs.python.org/3.7/library/decimal.html#module-decimal
    getcontext().prec = 4
    return Decimal(rate) * Decimal(payment.sender_amount)


def payment_with_conversion(payment: Payment) -> Payment:
    payment.receiver_amount = calculate_receiver_amount(payment)
    return payment


def formatted_payment(payment: Payment) -> Payment:
    payment.sender_amount_formatted = bn.format_currency(
        payment.sender_amount, payment.sender_currency)
    payment.receiver_amount_formatted = bn.format_currency(
        payment.receiver_amount, payment.receiver_currency)
    return payment


@api.get('/health')
def health():
    return


@api.post('/payment', response_model=Payment, tags=['payment'])
def create_payment(payment: Payment):
    return formatted_payment(payment_with_conversion(payment))

Let's give it a shot! Run the following commands to install requirements and start-up your service.

pip3 install -r requirements.txt
uvicorn main:api --reload

You should see the following.

$ uvicorn main:api --reload
INFO:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
INFO:     Started reloader process [55640]
INFO:     Started server process [55651]
INFO:     Waiting for application startup.
INFO:     Application startup complete.

And now you can kick off a sample request with the following command. You may need to install jq.

$ curl -s -X POST "localhost:8000/payment" -d '{
  "sender_id": "12892e99-b4c6-4f6b-b433-01996c1d8ec0",
  "receiver_id": "c0626613-60d5-4d7a-9f12-8865febcfa9f",
  "sender_amount": "20",
  "sender_currency": "USD",
  "receiver_currency": "EUR",
  "message": "Cheers 🍻"
}' | jq
{
  "message": "Cheers 🍻",
  "sender_id": "12892e99-b4c6-4f6b-b433-01996c1d8ec0",
  "sender_currency": "USD",
  "sender_amount": 20,
  "sender_amount_formatted": "$20.00",
  "receiver_id": "c0626613-60d5-4d7a-9f12-8865febcfa9f",
  "receiver_currency": "EUR",
  "receiver_amount_formatted": "€18.28",
  "receiver_amount": 18.28
}

Awesome! We have a simple service that does basic currency conversions. For this to be production ready, you'd want to implement a higher fidelity currency rate exchange service (i.e. something that gives real-time exchange rates), gunicorn for running workers and threads, a datastore for managing transactions and other payments, and some actual infrastructure to run it all.

Want to issue payments and connect bank accounts? Lots of the details are handled for you by stripe – would totally suggest checking them out. For other cool finance software, there's also a bunch of stuff on github you should check out.

I'll be covering more on that eventually but hope this covers the basis for creating a stateless API that works with financial data reasonably well.