Table Of Contents

Previous topic

Tallywallet - a Money framework in Python

This Page

Programming API

Currency

The currency module defines currencies of various types. As of this release, there are only a handful yet defined.

class tallywallet.common.currency.Currency[source]

This is an enumeration class which captures the codes defined in ISO 4217 for international currencies.

Trade

class tallywallet.common.trade.TradePath

TradePath(rcv, work, out)

A 3-tuple of Currency objects, describing the path of a foreign exchange trade. The first element is the source currency, the second the reference currency of the account, and the third the destination currency.

class tallywallet.common.trade.TradeGain

TradeGain(rcv, gain, out)

A 3-tuple of numerical values, describing the result of a change in foreign exchange rates. The first element is the value due to the prior rate, the second is the gain due to the rate change, and the third element is the final sum.

Exchange

The exchange module contains functionality to enable conversion between currencies.

class tallywallet.common.exchange.Exchange

An exchange is a lookup container for currency exchange rates.

It behaves just like a Python dictionary, but has some extra methods.

The approach Tallywallet uses is to associate each rate against a key which is a 2-tuple of Currency. By convention, the first element of this key is the source currency, and the second is the destination. The values of the exchange mapping can therefore be considered as gain from one currency to the next.

get(key)

Return the value stored against key.

This method overrides the standard behaviour of dict.get. It infers the values of missing keys as follows:

  • The rate of a currency against itself is unity.
  • The rate of one currency against another is the reciprocal of the reverse rate (if defined).
convert(val, path, fees=TradeFees(rcv=0, out=0))

Return the calculated outcome of converting the amount val via the TradePath path.

gain(val, path, prior=None, fees=TradeFees(rcv=0, out=0))

Calculate the gain related to a change in exchange rates. The self object contains the latest rates, and the historic ones are passed as an argument to this method.

Parameters:prior – An Exchange object which contains the rates existing prior to those in the self object.
Return type:TradeGain

Ledger

The ledger module defines Ledger and some associated classes.

class tallywallet.common.ledger.Role[source]

This enumeration contains definitions for the roles played by a column in a Ledger.

class tallywallet.common.ledger.Column

Column(name, currency, role)

A 3-tuple, describing a column in a Ledger.

class tallywallet.common.ledger.Ledger(ref, *args)[source]

This class implements the fundamental operations you need to perform Adjusted Cost Base accounting.

Parameters:
  • ref – (optional) the base Currency type for the Ledger
  • args – One or more Column objects
equation[source]

The Fundamental Accounting Equation is this:

Assets - Liabilities = Capital + Income - Expenses

(currency trading gains are counted as income). For practical purposes, it is often rearranged to be:

Assets + Expenses = Capital + Income + Liabilities

This property evaluates both sides of this second equation, and determines if they are equal or not.

Returns:A tuple of lhs, rhs, status
adjustments(exchange, cols=None)

Calculates the effects of a change in exchange rates.

Supply an Exchange object and an optional sequence of columns. If no columns are specified then all are assumed.

The columns are recalculated (but not committed) against the new exchange rates.

This method will generate a sequence of 3-tuples; (TradeGain, Column, Exchange).

This output is compatible with the arguments accepted by the commit method.

commit(trade, col, exchange=None, **kwargs)[source]

Applies a trade to the ledger.

If you supply an exchange argument, trade may be a TradeGain object. In this usage, a trading account in the currency of the ledger column will accept any exchange gain or loss.

Otherwise, trade should be a number. It will be added to the specified column in the ledger.

value(name)[source]

Returns the current value of a column in the ledger.

Parameters:name – The name of the column

Example

This example is taken from Peter Selinger’s tutorial on multiple currency accounting. We’ll use a Ledger to track the transactions set out below, which comprise both actual expenses and incurred gains/losses due to variations in currency rates.

The scenario is a brief trip from Canada to the USA, which requires buying US dollars, spending some during exchange rate fluctuations, and changing back to Canadian money at the end.

date   asset asset capital expense trading
Jan 1 Opening balance CAD 200 USD 0 CAD 200 CAD 0 USD 0 CAD 0
Jan 2 1 USD==1.20CAD CAD-120 USD 100 . . USD 100 CAD 120
. Balance CAD 80 USD 100 CAD 200 CAD 0 USD 100 CAD 120
Jan 3 1 USD==1.30CAD . USD-40 . CAD 52 USD-40 CAD 52
. Balance CAD 80 USD 60 CAD 200 CAD 52 USD 60 CAD-68
Jan 5 1 USD==1.25CAD CAD 75 USD-60 . . USD-60 CAD 75
. Balance CAD 155 USD 0 CAD 200 CAD 52 USD 0 CAD 07
Jan 7 Buy food CAD-20 . . CAD 20 .
. Balance CAD 135 USD 0 CAD 200 CAD 72 USD 0 CAD 07

Throughout, we’ll assume this boilerplate:

import datetime
from decimal import Decimal as Dl
from tallywallet.common.currency import Currency as Cy
from tallywallet.common.ledger import Ledger
from tallywallet.common.ledger import Role

Jan 1

First, we’ll establish a ledger with the necessary columns. We needn’t define a currency trading account; that will be created for us:

ldgr = Ledger(
    Column("Canadian cash", Cy.CAD, Role.asset),
    Column("US cash", Cy.USD, Role.asset),
    Column("Capital", Cy.CAD, Role.capital),
    Column("Expense", Cy.CAD, Role.expense),
    ref=Cy.CAD)

At this point, there’s no balance, and no rates defined for the currencies. ldgr.equation.status will evaluate to Status.failed.

The scenario begins with an opening balance of CAD 200. In double entry book-keeping, this becomes an asset on the left hand side, and capital on the right. We can define the opening balance like this:

for amount, col in zip(
    (Dl(200), Dl(0), Dl(200), Dl(0)), ldgr.columns.values()
):
    ldgr.commit(
        amount, col,
        ts=datetime.date(2013, 1, 1), note="Opening balance")

Jan 2

Let’s apply the initial currency exchange rate. We do that by instantiating an Exchange object, and creating a sequence of trades to apply to our ledger:

exchange = Exchange({(Cy.USD, Cy.CAD): Dl("1.2")})
for args in ldgr.adjustments(exchange):
    ldgr.commit(
        *args, ts=datetime.date(2013, 1, 2),
        note="1 USD = 1.20 CAD")

The next step is to buy CAD 120 worth of US dollars. How many USD will that give us?:

usd = exchange.convert(120, TradePath(Cy.CAD, Cy.CAD, Cy.USD))

So we add that to our US cash account:

ldgr.commit(usd, ldgr.columns["US cash"])

If we should check ldgr.equation.status we’d get Status.failed. Our book is unbalanced. We must remember to deduct from our Canadian cash:

ldgr.commit(-120, ldgr.columns["Canadian cash"])

And now, ldgr.equation.status will evaluate to Status.ok.

Jan 3

Our book holds both American and Canadian dollars, so it’s exposed to fluctuations in the exchange rate. On 3rd January, you can get $1.30 Canadian for one American greenback. Let’s apply that to our ledger:

exchange = Exchange({(Cy.USD, Cy.CAD): Dl("1.3")})
for args in ldgr.adjustments(exchange):
    ldgr.commit(
        *args, ts=datetime.date(2013, 1, 3),
        note="1 USD = 1.30 CAD")

At this point in the story, we spend forty US dollars. On the left hand side of the ledger, it’s clear that this needs to come from our US assets. But on the right hand side, our expense account is in Canadian dollars. So we need to make a calculation to determine how much to deduct from there:

cad = exchange.convert(40, TradePath(Cy.USD, Cy.CAD, Cy.CAD))

And so:

ldgr.commit(-40, ldgr.columns["US cash"])
ldgr.commit(cad, ldgr.columns["Expense"])

Jan 5

The exchange rate shifts a bit today in favour of the Canadian dollar. We don’t make any purchases, but we do convert all our US dollars back to Canadian. So there should be no change to the right hand side of our ledger, only movement of our assets on the left.

First we apply the new rate:

exchange = Exchange({(Cy.USD, Cy.CAD): Dl("1.25")})
for args in ldgr.adjustments(exchange):
    ldgr.commit(
        *args, ts=datetime.date(2013, 1, 5),
        note="1 USD = 1.25 CAD")

... then work out how much our US dollars are worth:

usd = ldgr.value("US cash")
cad = exchange.convert(usd, TradePath(Cy.USD, Cy.CAD, Cy.CAD))

... and finish by committing that to our book:

ldgr.commit(-usd, ldgr.columns["US cash"])
ldgr.commit(cad, ldgr.columns["Canadian cash"])

Jan 7

We are now back in Canada but stuck in the airport waiting for our transfer home. We want food. So we cough up twenty dollars for a nasty burger and a bottle of fizzy beer. Here’s the transaction for that:

ldgr.commit(-20, cols["Canadian cash"], note="Buy food")
ldgr.commit(20, cols["Expense"], note="Buy food")

How much money do we have left? ldgr.value("Canadian cash") says $135.00. Looking at the other columns it seems we spent CAD 72.00 during our trip. We accidentally made CAD 7.00 due to the fluctuations in the exchange rate while we were away.