Chapter 8. Evaluating a Portfolio

Money itself isn’t lost or made, it’s simply transferred from one perception to another. Like magic.

Gordon Gekko, Wall Street (the movie)

We’ve dallied around the question of how to convert the several Money s in a Portfolio into a single currency. Let’s dally no longer!

The next feature on our list is the one dealing with mixed currencies:

5 USD x 2 = 10 USD

10 EUR x 2 = 20 EUR

4002 KRW / 4 = 1000.5 KRW

5 USD + 10 USD = 15 USD

Separate test code from production code

Remove redundant tests

5 USD + 10 EUR = 17 USD

1 USD + 1100 KRW = 2200 KRW

Mixing money

A heterogeneous combination of currencies demands that we create a new abstraction in our code: the conversion of money from one currency to another. This requires establishing some ground rules about currency conversions, drawn from our problem domain:

  1. Conversion always relates a pair of currencies. This is important because we want all conversions to be independent. It does happen in reality that multiple currencies are “pegged” to one single currency — which means that a particular exchange rate is fixed de jure. 1 Even in such cases, it’s important to treat each pegged relationship as a distinct pair.

  2. Conversion is from one currency to another with a well defined exchange rate. The exchange rate — the number of units of the “to” currency we get for one unit of the “from” currency — is a key component of currency conversion. The exchange rate is represented by a fractional number.

  3. The two exchange rates between a pair currencies may or may not be arithmetical reciprocals of each other. For example: the exchange rate from EUR to USD may or may not be the mathematical reciprocal (i.e. 1⁠/⁠x) of the exchange rate from USD to EUR.

  4. It is possible for a currency to have no defined exchange rate to another currency. This could be because one of the two currencies is an inconvertible currency. 2

Given that currency conversion involves all the above considerations, how should we implement it? The answer is: one test-driven scenario at a time!

We’ll start by test-driving the scenario listed in the next item on our feature list: the conversion from EUR to USD. This will help us set up the scaffolding for the “convert” method and a single exchange rate from EUR to USD. Because exchange rates are unidirectional, we’ll represent this particular one as “EUR→USD”.

Starting with this one scenario means it’s likely that we’ll add more items to our feature list. That’s all right — making controlled progress at a measured pace isn’t a bad deal!

Go

Let’s write our new test in money_test.go to represent addition of Dollars and Euros:

func TestAdditionOfDollarsAndEuros(t *testing.T) {
    var portfolio s.Portfolio

    fiveDollars := s.NewMoney(5, "USD")
    tenEuros := s.NewMoney(10, "EUR")

    portfolio = portfolio.Add(fiveDollars)
    portfolio = portfolio.Add(tenEuros)

    expectedValue := s.NewMoney(17, "USD") 1
    actualValue := portfolio.Evaluate("USD")

    assertEqual(t, expectedValue, actualValue)
}
1

The expected value of 17 USD assumes that we get 1.2 dollars for every euro we convert

The test creates two Money struct`s representing 5 USD and 10 EUR, respectively. They are added to a newly created `Portfolio struct. The actualValue from evaluating the Portfolio in dollars is compared with the expectedValue struct of 17 USD.

The test fails as expected:

... Expected  [{amount:17 currency:USD}] Got: [{amount:15 currency:USD}]

This validates what we know: the evaluate method simply adds the amounts of all Money `struct`s (5 and 10 in our test) to get the result, regardless of the currencies involved (USD and EUR, respectively, in our test).

What we need is to first convert the amount of each Money into the target currency and then add it.

    for _, m := range p { 1
        total = total + convert(m, currency)
    }
1

In Evaluate method

How should we write the convert method? The simplest thing that works is to return the amount when the currencies match and to arbitrarily multiply by the conversion rate required by our test otherwise:

func convert(money Money, currency string) float64 { 1
    if money.currency == currency {
        return money.amount
    }
    return money.amount * 1.2 2
}
1

New function in portfolio.go file

2

Hard-coded exchange rate

The test turns green, but something doesn’t seem right about our code! Specifically:

  1. The exchange rate is hard-coded. It should be declared as a variable.

  2. The exchange rate isn’t dependent on the currency. It should be looked up based on the two currencies involved.

  3. The exchange rate should be modifiable.

Let’s address the first of these and add the remaining two to our feature list. We define a variable named eurToUsd in our convert method and use it:

func convert(money Money, currency string) float64 {
    eurToUsd := 1.2 1
    if money.currency == currency {
        return money.amount
    }
    return money.amount * eurToUsd 2
}
1

Exchange rate is defined as an appropriately named variable

2

The exchange rate variable is used to convert currency

The test is still green.

JavaScript

Let’s start by adding a new test in MoneyTest to test the addition of Dollars and Euros:

  testAdditionOfDollarsAndEuros() {
    let fiveDollars = new Money(5, "USD");
    let tenEuros = new Money(10, "EUR");
    let portfolio = new Portfolio();
    portfolio.add(fiveDollars, tenEuros);
    let expectedValue = new Money(17, "USD"); 1
    assert.deepStrictEqual(portfolio.evaluate("USD"), expectedValue);
  }
1

The expected value of 17 USD assumes that we get 1.2 dollars for every euro we convert

The test creates two Money objects representing 5 USD and 10 EUR each. These are added to a Portfolio object. The value from evaluating the Portfolio in USD is compared with a Money object representing 17 USD.

The test fails as expected:

AssertionError [ERR_ASSERTION]: Expected values to be strictly deep-equal:
+ actual - expected

  Money {
+   amount: 15,
-   amount: 17,
    currency: 'USD'
  }

We expect this failure because the current implementation of evaluate method simply adds the amount attribute of all Money objects, regardless of their currencies.

We need to first convert the amount of each Money into the target currency and then sum it.

    evaluate(currency) {
        let total = this.moneys.reduce((sum, money) => {
            return sum + this.convert(money, currency);
        }, 0);
        return new Money(total, currency);
    }

How should the convert method work? For now, the simplest implementation that works is one that returns amount when the currencies match and otherwise multiplies the amount by the conversion rate require by our test:

    convert(money, currency) { 1
        if (money.currency === currency) {
            return money.amount;
        }
        return money.amount * 1.2; 2
    }
1

New method in Portfolio class

2

Hard-coded exchange rate

The test is now green. That’s progress, but not everything is hunky-dory. In particular:

  1. The exchange rate is hard-coded. It should be declared as a variable.

  2. The exchange rate isn’t dependent on the currency. It should be looked up based on the two currencies involved.

  3. The exchange rate should be modifiable.

Let’s address the first of these right away and add the remaining to our feature list.

We define a variable named eurToUsd and use it in our convert method:

    convert(money, currency) {
        let eurToUsd = 1.2; 1
        if (money.currency === currency) {
            return money.amount;
        }
        return money.amount * eurToUsd; 2
    }
1

Exchange rate is defined as an appropriately named variable

2

The exchange rate variable is used to convert currency

All tests are green.

Python

Let’s write a new test in test_money.py that validates adding Dollars to Euros:

    def testAdditionOfDollarsAndEuros(self):
        fiveDollars = Money(5, "USD")
        tenEuros = Money(10, "EUR")
        portfolio = Portfolio()
        portfolio.add(fiveDollars, tenEuros)
        expectedValue = Money(17, "USD") 1
        actualValue = portfolio.evaluate("USD")
        self.assertEqual(expectedValue, actualValue)
1

The expected value of 17 USD assumes that we get 1.2 dollars for every euro we convert

The test creates two Money objects representing 5 USD and 10 EUR, respectively. They are added to a pristine Portfolio object. The actualValue from evaluating the Portfolio in dollars is compared with a newly minted expectedValue of 17 USD.

We expect the test to fail, of course, because we are in the RED phase of our of RGR cycle. However, the error message from the assertion failure is rather cryptic:

AssertionError: <money.Money object at 0x10f3c3280> != <money.Money object at 0x10f3c33a0>

Who on earth knows what mysterious goblins reside at those obscure memory addresses!

This is one of those times where we must slow down and write a better failing test before we attempt to get to GREEN. Can we make the assertion statement print a more helpful error message?

The assertEqual method — like most other assertion methods in the unittest package — takes an optional third parameter, which is a custom error message. Let’s provide a formatted string showing the stringified representation of expectedValue and actualValue:

self.assertEqual(expectedValue, actualValue,
                "%s != %s"%(expectedValue, actualValue)) 1
1

Last line of testAdditionOfDollarsAndEuros test method

Nope! That simply prints the obscure memory addresses twice:

AssertionError:
    <money.Money object at 0x1081111f0> != <money.Money object at 0x108111310> :
    <money.Money object at 0x1081111f0> != <money.Money object at 0x108111310>

What we need to do is to override the __str__ method in the Money class and make it return a more human-readable representation. Something like “USD 17.00”.

    def __str__(self): 1
        return f"{self.currency} {self.amount:0.2f}"
1

In Money class

We format Money’s currency and amount fields, printing the latter up to two decimal places.

After adding the __str__ method, let’s run our test suite again:

AssertionError: ... USD 17.00 != USD 15.00

Ah, much better! Seventeen dollars certainly aren’t the same thing as 15 dollars!

Tip

Python’s F-strings interpolation provides a succinct and neat way to format strings with a mixture of fixed text and variables. F-strings were defined in [PEP-498]https://www.python.org/dev/peps/pep-0498/ and have been a part of Python since version 3.6.

This validates our belief that the evaluate method, as currently implemented, mindlessly adds the amounts of all Money objects (5 and 10 in our test) to get the result, with no regard to the currencies (USD and EUR, respectively, in our test).

A closer examination of the evaluate method shows that the mindlessness is in the lambda expression. It maps every Money object to its amount, regardless of its currency. These amounts are then added up by the reduce function using the add operator.

What if the lambda expression mapped every Money object to its converted value? The target currency for the conversion would be the currency in which the Portfolio is being evaluated.

        total = functools.reduce(operator.add,
          map(lambda m: self.__convert(m, currency), self.moneys), 0) 1
1

In Money class

Important

Python doesn’t have truly “Private” scope for variables or functions. The naming convention and something called “name mangling” ensure that names with two leading underscores are treated as private.

How should we implement the __convert method? Converting to the same currency as that of the Money is trivial: the Money’s amount doesn’t change in this case. When converting to a different currency, we’ll multiply Money’s amount with the (for now) hard-coded exchange rate between USD and EUR:

    def __convert(self, aMoney, aCurrency): 1
        if aMoney.currency == aCurrency:
            return aMoney.amount
        else:
            return aMoney.amount * 1.2 2
1

New method in Portfolio class

2

Hard-coded exchange rate

The test is green. Yay … and hmm! We should do the refactoring to remove the ugliness of this code. Here are some problems with it:

  1. The exchange rate is hard-coded. It should be declared as a variable.

  2. The exchange rate isn’t dependent on the currency. It should be looked up based on the two currencies involved.

  3. The exchange rate should be modifiable.

Let’s address the first of these three items in the REFACTOR phase and add the remaining two to our feature list.

We define a private variable named _eur_to_usd in the __init__ method and use it instead of the hard-coded value in the __convert method:

class Portfolio:
    def __init__(self):
        self.moneys = []
        self._eur_to_usd = 1.2 1
...
    def __convert(self, aMoney, aCurrency):
        if aMoney.currency == aCurrency:
            return aMoney.amount
        else:
            return aMoney.amount * self._eur_to_usd 2
1

Exchange rate is defined as an appropriately named variable

2

The exchange rate variable is used to convert currency

All tests are green.

Committing our changes

We have our first implementation of converting money between two different currencies, specifically USD → EUR. Let’s commit our changes to our local Git repository:

git add .
git commit -m "feat: convert from EUR to USD"

Where We Are

We have solved the conversion of Money s in different currencies for the scenario of converting USD to EUR. However, we cut a few corners while doing so. The conversion only works for one specific case (USD → EUR). Furthermore, there is no way to add or modify exchange rates.

Let’s update our feature list to cross out the accomplished items and add the new ones.

5 USD x 2 = 10 USD

10 EUR x 2 = 20 EUR

4002 KRW / 4 = 1000.5 KRW

5 USD + 10 USD = 15 USD

Separate test code from production code

Remove redundant tests

5 USD + 10 EUR = 17 USD

1 USD + 1100 KRW = 2200 KRW

Determine exchange rate based on the currencies involved (from → to)

Allow exchange rates to be modified

1 For an economic discussion of currency pegging, see https://www.investopedia.com/terms/c/currency-peg.asp

2 Currencies can be inconvertible for a variety of reasons: political, economic, or even military. https://www.investopedia.com/terms/i/inconvertible_currency.asp

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset