Search icon CANCEL
Subscription
0
Cart icon
Your Cart (0 item)
Close icon
You have no products in your basket yet
Save more on your purchases! discount-offer-chevron-icon
Savings automatically calculated. No voucher code required.
Arrow left icon
All Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Newsletter Hub
Free Learning
Arrow right icon
timer SALE ENDS IN
0 Days
:
00 Hours
:
00 Minutes
:
00 Seconds
Arrow up icon
GO TO TOP
Test Driven Python Development

You're reading from   Test Driven Python Development Develop high-quality and maintainable Python applications using the principles of test-driven development

Arrow left icon
Product type Paperback
Published in Apr 2015
Publisher Packt
ISBN-13 9781783987924
Length 264 pages
Edition 1st Edition
Languages
Tools
Arrow right icon
Author (1):
Arrow left icon
Siddharta Govindaraj Siddharta Govindaraj
Author Profile Icon Siddharta Govindaraj
Siddharta Govindaraj
Arrow right icon
View More author details
Toc

Table of Contents (20) Chapters Close

Test-Driven Python Development
Credits
About the Author
Acknowledgments
About the Reviewers
www.PacktPub.com
Preface
1. Getting Started with Test-Driven Development FREE CHAPTER 2. Red-Green-Refactor – The TDD Cycle 3. Code Smells and Refactoring 4. Using Mock Objects to Test Interactions 5. Working with Legacy Code 6. Maintaining Your Test Suite 7. Executable Documentation with doctest 8. Extending unittest with nose2 9. Unit Testing Patterns 10. Tools to Improve Test-Driven Development Answers to Exercises Working with Older Python Versions Index

Code Smells and Refactoring


This exercise asks us to refactor the Stock class and extract all the moving average related calculations into a new class.

The following is the code that we start with:

def get_crossover_signal(self, on_date):
    NUM_DAYS = self.LONG_TERM_TIMESPAN + 1
    closing_price_list = \
        self.history.get_closing_price_list(on_date, NUM_DAYS)

    if len(closing_price_list) < NUM_DAYS:
        return StockSignal.neutral

    long_term_series = \
        closing_price_list[-self.LONG_TERM_TIMESPAN:]
    prev_long_term_series = \
        closing_price_list[-self.LONG_TERM_TIMESPAN-1:-1]
    short_term_series = \
        closing_price_list[-self.SHORT_TERM_TIMESPAN:]
    prev_short_term_series = \
        closing_price_list[-self.SHORT_TERM_TIMESPAN-1:-1]

    long_term_ma = sum([update.value
                        for update in long_term_series])\
                    /self.LONG_TERM_TIMESPAN
    prev_long_term_ma = sum([update.value
                             for update in prev_long_term_series])\
                         /self.LONG_TERM_TIMESPAN
    short_term_ma = sum([update.value
                         for update in short_term_series])\
                    /self.SHORT_TERM_TIMESPAN
    prev_short_term_ma = sum([update.value
                              for update in prev_short_term_series])\
                         /self.SHORT_TERM_TIMESPAN

    if self._is_crossover_below_to_above(prev_short_term_ma,
                                         prev_long_term_ma,
                                         short_term_ma,
                                         long_term_ma):
                return StockSignal.buy

    if self._is_crossover_below_to_above(prev_long_term_ma,
                                         prev_short_term_ma,
                                         long_term_ma,
                                         short_term_ma):
                return StockSignal.sell

    return StockSignal.neutral

As we can see, there are a number of calculations relating to identifying the moving average window and then calculating the moving average value. These calculations really deserve to be in their own class.

To start with, we create an empty MovingAverage class as follows:

class MovingAverage:
    pass

Now we need to make a design decision on how we want this class to be used. Let us decide that the class should take an underlying timeseries and should be able to compute the moving average at any point based on that timeseries. With this design, the class needs to take the timeseries and the duration of the moving average as parameters, as shown in the following:

def __init__(self, series, timespan):
    self.series = series
    self.timespan = timespan

We can now extract the moving average calculation into this class as follows:

class MovingAverage:
    def __init__(self, series, timespan):
        self.series = series
        self.timespan = timespan

    def value_on(self, end_date):
        moving_average_range = self.series.get_closing_price_list(
                                   end_date, self.timespan)
        if len(moving_average_range) < self.timespan:
            raise NotEnoughDataException("Not enough data")
        price_list = [item.value for item in moving_average_range]
        return sum(price_list)/len(price_list)

This is the same moving average calculation code from Stock.get_signal_crossover. The only notable point is that an exception is raised if there is not enough data to perform the calculation. Let us define this exception in the timeseries.py file as follows:

class NotEnoughDataException(Exception):
    pass

We can now use this method in Stock.get_signal_crossover as follows:

def get_crossover_signal(self, on_date):
    prev_date = on_date - timedelta(1)
    long_term_ma = \
        MovingAverage(self.history, self.LONG_TERM_TIMESPAN)
    short_term_ma = \
        MovingAverage(self.history, self.SHORT_TERM_TIMESPAN)

    try:
        long_term_ma_value = long_term_ma.value_on(on_date)
        prev_long_term_ma_value = long_term_ma.value_on(prev_date)
        short_term_ma_value = short_term_ma.value_on(on_date)
        prev_short_term_ma_value = short_term_ma.value_on(prev_date)
    except NotEnoughDataException:
        return StockSignal.neutral

    if self._is_crossover_below_to_above(prev_short_term_ma_value,
                                         prev_long_term_ma_value,
                                         short_term_ma_value,
                                         long_term_ma_value):
                return StockSignal.buy

    if self._is_crossover_below_to_above(prev_long_term_ma_value,
                                         prev_short_term_ma_value,
                                         long_term_ma_value,
                                         short_term_ma_value):
                return StockSignal.sell

    return StockSignal.neutral

Run the tests, and all 21 tests should pass.

Once we extract the calculation to a class, we find that the temporary variables that we created during Replace Calculation with Temporary Variable section in Chapter 3, Code Smells and Refactoring are not really required. The code is equally self-explanatory without them, so we can now get rid of them, as shown in the following:

def get_crossover_signal(self, on_date):
    prev_date = on_date - timedelta(1)
    long_term_ma = \
        MovingAverage(self.history, self.LONG_TERM_TIMESPAN)
    short_term_ma = \
        MovingAverage(self.history, self.SHORT_TERM_TIMESPAN)

    try:
        if self._is_crossover_below_to_above(
                short_term_ma.value_on(prev_date),
                long_term_ma.value_on(prev_date),
                short_term_ma.value_on(on_date),
                long_term_ma.value_on(on_date)):
            return StockSignal.buy

        if self._is_crossover_below_to_above(
                long_term_ma.value_on(prev_date),
                short_term_ma.value_on(prev_date),
                long_term_ma.value_on(on_date),
                short_term_ma.value_on(on_date)):
            return StockSignal.sell
    except NotEnoughDataException:
        return StockSignal.neutral

    return StockSignal.neutral

A final cleanup: now that we have moving average classes, we can replace the parameters to the _is_crossover_below_to_above method to take the moving average class instead of the individual values. The method now becomes as follows:

def _is_crossover_below_to_above(self, on_date, ma, reference_ma):
    prev_date = on_date - timedelta(1)
    return (ma.value_on(prev_date)
                < reference_ma.value_on(prev_date)
            and ma.value_on(on_date)
                > reference_ma.value_on(on_date))

And we can change the get_crossover_signal method to call this with the new parameters as follows:

def get_crossover_signal(self, on_date):
    long_term_ma = \
        MovingAverage(self.history, self.LONG_TERM_TIMESPAN)
    short_term_ma = \
        MovingAverage(self.history, self.SHORT_TERM_TIMESPAN)

    try:
        if self._is_crossover_below_to_above(
                on_date,
                short_term_ma,
                long_term_ma):
            return StockSignal.buy

        if self._is_crossover_below_to_above(
                on_date,
                long_term_ma,
                short_term_ma):
            return StockSignal.sell
    except NotEnoughDataException:
        return StockSignal.neutral

    return StockSignal.neutral

With this, our Extract Class refactoring is complete.

The get_crossover_signal class is now extremely easy to read and understand.

Notice how the design for the MovingAverage class builds on top of the TimeSeries class that we extracted earlier. As we refactor code and extract classes, we often find that the many classes get reused in other contexts. This is the advantage of having small classes with a single responsibility.

The refactoring into a separate class also allowed us to remove the temporary variables that we had created earlier, and made the parameters for the crossover condition much simpler. Again, these are side effects of having small classes with single responsibilities.

lock icon The rest of the chapter is locked
Register for a free Packt account to unlock a world of extra content!
A free Packt account unlocks extra newsletters, articles, discounted offers, and much more. Start advancing your knowledge today.
Unlock this book and the full library FREE for 7 days
Get unlimited access to 7000+ expert-authored eBooks and videos courses covering every tech area you can think of
Renews at $15.99/month. Cancel anytime
Visually different images