Advanced Trading Infrastructure - Position Class

Advanced Trading Infrastructure - Position Class

At the end of last year I announced that I would be working on a series of articles regarding the development of an Advanced Trading Infrastructure. Since the initial announcement I haven't mentioned the project to any great extent. However, in this article I want to discuss the progress I've made to date.

At the outset I decided that I wanted to re-use and improve upon as much of the codebases of the event-driven backtester and QSForex as possible in order to avoid duplicated effort. Despite a solid attempt to make use of large parts of these two codebases, I came to the conclusion that it was necessary to "start from scratch" with how the position handling/sizing, risk management and portfolio construction was handled.

In effect I've modified the design of QSForex and the event-driven backtesters to include proper integration of position sizing and risk management layers, which were not present in previous versions. These two components provide robust portfolio construction tools that aid in crafting "professional" portfolios, rather than those found in a simplistic vectorised backtester.

At this stage I have not actually written any specific risk management or position sizing logic. Since this logic is highly specific to an individual or firm, I have decided that I will provide example modules for straightforward situations such as Kelly Criterion positioning or volatility risk levels, but will leave complicated risk and position management overlays to be developed by users of the software. This allows you to use the "off the shelf" versions that I develop, but also to completely swap out these components with your own custom logic as you see fit. This way the software is not forcing you down a particular risk management methodology.

To date I have coded the basics of a portfolio management system. The entire backtesting and trading tool, which I am tentatively naming QSTrader, is far from being production-ready. In fact, I would go so far as to say it is in a "pre-alpha" state at this stage! Despite the fact that the system is early in its development, I have put significant effort into unit-testing the early components, as well as testing against external brokerage-calculated values. I am currently quite confident in the position handling mechanism. However, if edge cases do arrive, they can simply be added as unit tests to further improve robustness.

The project is now available (in an extremely early state!) at https://www.github.com/mhallsmoore/qstrader.git under a liberal open-source MIT license. At this stage it lacks documentation, so I am only adding the link for those who wish to browse the code as it stands.

Component Design

In the previous article in the series on Advanced Trading Infrastructure I discussed the broad roadmap for development. In this article I want to discuss one of the most important aspects of the system, namely the Position component, that will form the basis of the Portfolio and subsequently PortfolioHandler.

When designing such a system it is necessary to consider how we will break down separate behaviours into various submodules. Not only does this prevent strong coupling of the components, but it also allows much more straightforward testing, as each component can be unit tested separately.

The design I have chosen for the portfolio management infrastructure consists of the following components:

  • Position - This class encapsulates all data associated with an open position in an asset. That is, it tracks the realised and unrealised profit and loss (PnL) by averaging the multiple "legs" of the transaction, inclusive of transaction costs.
  • Portfolio - The Portfolio class that encapsulates a list of Positions, as well as a cash balance, equity and PnL.
  • PositionSizer - The PositionSizer class provides the PortfolioHandler (see below) with guidance on how to size positions once a strategy signal is received. For instance, the PositionSizer could incorporate a Kelly Criterion approach.
  • RiskManager - The RiskManager is used by the PortfolioHandler to verify, modify or veto any suggested trades that pass through from the PositionSizer, based on the current composition of the portfolio and external risk considerations (such as correlation to indices or volatility).
  • PortfolioHandler - The PortfolioHandler class is responsible for the management of the current Portfolio, interacting with the RiskManager and PositionSizer as well as submitting orders to be executed by an execution handler.

Note that this component organisation is somewhat different to how the backtesting system operates in QSForex. In that case the Portfolio object is the equivalent of the PortfolioHandler class above. I've split these into two here. This allows much more straightforward risk management and position sizing with the RiskManager and PositionSizer classes.

We will now turn our attention to the Position class. In later articles we will look at the Portfolio, PortfolioHandler and the risk/position sizing components.

Position

The Position class is very similar to its namesake in the QSForex project, with the exception that it has initially been designed for use with equity, rather than forex, instruments. Hence there is no notion of a "base" or "quote" currency. However, we retain the ability to update the unrealised PnL on demand, via updating of the current bid and ask prices quoted on the market.

I'll output the code listing in full and then run through how it works.

Note that any of these listings are subject to change, since I will be continually making changes to this project. Eventually I hope others will collaborate by providing Pull Requests to the codebase.

position.py

from decimal import Decimal


TWOPLACES = Decimal("0.01")
FIVEPLACES = Decimal("0.00001")


class Position(object):
    def __init__(
        self, action, ticker, init_quantity,
        init_price, init_commission,
        bid, ask
    ):
        """
        Set up the initial "account" of the Position to be
        zero for most items, with the exception of the initial
        purchase/sale.

        Then calculate the initial values and finally update the
        market value of the transaction.
        """
        self.action = action
        self.ticker = ticker
        self.quantity = init_quantity
        self.init_price = init_price
        self.init_commission = init_commission

        self.realised_pnl = Decimal("0.00")
        self.unrealised_pnl = Decimal("0.00")

        self.buys = Decimal("0")
        self.sells = Decimal("0")
        self.avg_bot = Decimal("0.00")
        self.avg_sld = Decimal("0.00")
        self.total_bot = Decimal("0.00")
        self.total_sld = Decimal("0.00")
        self.total_commission = init_commission

        self._calculate_initial_value()
        self.update_market_value(bid, ask)

    def _calculate_initial_value(self):
        """
        Depending upon whether the action was a buy or sell ("BOT"
        or "SLD") calculate the average bought cost, the total bought
        cost, the average price and the cost basis.

        Finally, calculate the net total with and without commission.
        """

        if self.action == "BOT":
            self.buys = self.quantity
            self.avg_bot = self.init_price.quantize(FIVEPLACES)
            self.total_bot = (self.buys * self.avg_bot).quantize(TWOPLACES)
            self.avg_price = (
                (self.init_price * self.quantity + self.init_commission)/self.quantity
            ).quantize(FIVEPLACES)
            self.cost_basis = (
                self.quantity * self.avg_price
            ).quantize(TWOPLACES)
        else:  # action == "SLD"
            self.sells = self.quantity
            self.avg_sld = self.init_price.quantize(FIVEPLACES)
            self.total_sld = (self.sells * self.avg_sld).quantize(TWOPLACES)
            self.avg_price = (
                (self.init_price * self.quantity - self.init_commission)/self.quantity
            ).quantize(FIVEPLACES)
            self.cost_basis = (
                -self.quantity * self.avg_price
            ).quantize(TWOPLACES)
        self.net = self.buys - self.sells
        self.net_total = (self.total_sld - self.total_bot).quantize(TWOPLACES)
        self.net_incl_comm = (self.net_total - self.init_commission).quantize(TWOPLACES)

    def update_market_value(self, bid, ask):
        """
        The market value is tricky to calculate as we only have
        access to the top of the order book through Interactive
        Brokers, which means that the true redemption price is
        unknown until executed.

        However, it can be estimated via the mid-price of the
        bid-ask spread. Once the market value is calculated it
        allows calculation of the unrealised and realised profit
        and loss of any transactions.
        """
        midpoint = (bid+ask)/Decimal("2.0")
        self.market_value = (
            self.quantity * midpoint
        ).quantize(TWOPLACES)
        self.unrealised_pnl = (
            self.market_value - self.cost_basis
        ).quantize(TWOPLACES)
        self.realised_pnl = (
            self.market_value + self.net_incl_comm
        )

    def transact_shares(self, action, quantity, price, commission):
        """
        Calculates the adjustments to the Position that occur
        once new shares are bought and sold.

        Takes care to update the average bought/sold, total
        bought/sold, the cost basis and PnL calculations,
        as carried out through Interactive Brokers TWS.
        """
        prev_quantity = self.quantity
        prev_commission = self.total_commission

        self.total_commission += commission

        # Adjust total bought and sold
        if action == "BOT":
            self.avg_bot = (
                (self.avg_bot*self.buys + price*quantity)/(self.buys + quantity)
            ).quantize(FIVEPLACES)
            if self.action != "SLD":
                self.avg_price = (
                    (
                        self.avg_price*self.buys +
                        price*quantity+commission
                    )/(self.buys + quantity)
                ).quantize(FIVEPLACES)
            self.buys += quantity
            self.total_bot = (self.buys * self.avg_bot).quantize(TWOPLACES)

        # action == "SLD"
        else:
            self.avg_sld = (
                (self.avg_sld*self.sells + price*quantity)/(self.sells + quantity)
            ).quantize(FIVEPLACES)
            if self.action != "BOT":
                self.avg_price = (
                    (
                        self.avg_price*self.sells +
                        price*quantity-commission
                    )/(self.sells + quantity)
                ).quantize(FIVEPLACES)
            self.sells += quantity
            self.total_sld = (self.sells * self.avg_sld).quantize(TWOPLACES)

        # Adjust net values, including commissions
        self.net = self.buys - self.sells
        self.quantity = self.net
        self.net_total = (
            self.total_sld - self.total_bot
        ).quantize(TWOPLACES)
        self.net_incl_comm = (
            self.net_total - self.total_commission
        ).quantize(TWOPLACES)

        # Adjust average price and cost basis
        self.cost_basis = (
            self.quantity * self.avg_price
        ).quantize(TWOPLACES)

You'll notice that the entire project makes extensive use of the decimal module. This is an absolute necessity in financial applications, otherwise you end up causing significant rounding errors due to the mathematics of floating point handling.

I've created two helper variables, TWOPLACES and FIVEPLACES, which are used subsequently to define the level of precision necessary for rounding in calculations.

TWOPLACES = Decimal("0.01")
FIVEPLACES = Decimal("0.00001")

The Position class requires a transaction action: "Buy" or "Sell". I've used the Interactive Brokers code BOT and SLD for these throughout the code. In addition, the Position requires a ticker symbol, a quantity to transact, the price of purchase or sale and the commission.

class Position(object):
    def __init__(
        self, action, ticker, init_quantity,
        init_price, init_commission,
        bid, ask
    ):
        """
        Set up the initial "account" of the Position to be
        zero for most items, with the exception of the initial
        purchase/sale.
        Then calculate the initial values and finally update the
        market value of the transaction.
        """
        self.action = action
        self.ticker = ticker
        self.quantity = init_quantity
        self.init_price = init_price
        self.init_commission = init_commission

The Position also keeps track of a few other metrics, which fully mirror those handled by Interactive Brokers. It tracks the PnL, the quantity of buys and sells, the average purchase price and the average sale price, the total purchase price and the total sale price, as well as the total commission spent to date.

# position.py

        self.realised_pnl = Decimal("0.00")
        self.unrealised_pnl = Decimal("0.00")

        self.buys = Decimal("0")
        self.sells = Decimal("0")
        self.avg_bot = Decimal("0.00")
        self.avg_sld = Decimal("0.00")
        self.total_bot = Decimal("0.00")
        self.total_sld = Decimal("0.00")
        self.total_commission = init_commission

The Position class has been structured this way because it is very difficult to pin down the idea of a "trade". For instance, let's imagine we carry out the following transactions:

  • Day 1 - Purchase 100 shares of GOOG. Total 100.
  • Day 2 - Purchase 200 shares of GOOG. Total 300.
  • Day 3 - Sell 400 shares of GOOG. Total -100.
  • Day 4 - Purchase 200 shares of GOOG. Total 100.
  • Day 5 - Sell 100 shares of GOOG. Total 0.

This constitutes a "round trip". How are we to determine the profit on this? Do we work out the profit on every leg of the transaction? Do we only calculate profit once the quantity nets out back to zero?

These problems are solved by using a realised and unrealised PnL. We will always know how much we have made to date and how much we might make by keeping track of these two values. At the Portfolio level we can simply total these values up and work out our total PnL at any time.

Finally, in the initialisation method __init__, we calculate the initial values and update the market value by the latest bid/ask:

# position.py

        self._calculate_initial_value()
        self.update_market_value(bid, ask)

The _calculate_initial_value method is as follows:

# position.py

   def _calculate_initial_value(self):
        """
        Depending upon whether the action was a buy or sell ("BOT"
        or "SLD") calculate the average bought cost, the total bought
        cost, the average price and the cost basis.
        Finally, calculate the net total with and without commission.
        """

        if self.action == "BOT":
            self.buys = self.quantity
            self.avg_bot = self.init_price.quantize(FIVEPLACES)
            self.total_bot = (self.buys * self.avg_bot).quantize(TWOPLACES)
            self.avg_price = (
                (self.init_price * self.quantity + self.init_commission)/self.quantity
            ).quantize(FIVEPLACES)
            self.cost_basis = (
                self.quantity * self.avg_price
            ).quantize(TWOPLACES)
        else:  # action == "SLD"
            self.sells = self.quantity
            self.avg_sld = self.init_price.quantize(FIVEPLACES)
            self.total_sld = (self.sells * self.avg_sld).quantize(TWOPLACES)
            self.avg_price = (
                (self.init_price * self.quantity - self.init_commission)/self.quantity
            ).quantize(FIVEPLACES)
            self.cost_basis = (
                -self.quantity * self.avg_price
            ).quantize(TWOPLACES)
        self.net = self.buys - self.sells
        self.net_total = (self.total_sld - self.total_bot).quantize(TWOPLACES)
        self.net_incl_comm = (self.net_total - self.init_commission).quantize(TWOPLACES)

This method carries out the initial calculations upon opening a new position. For "BOT" actions (purchases), the number of buys is incremented, the average and total bought values are calculated, as is the average price of the position. The cost basis is calculated as the current quantity multiplied by the average price paid. Hence it is the "total price paid so far". In addition, the net quantity, net total and net including commission are also calculated.

The next method is update_market_value. This is a tricky method to implement as it relies on a particular choice for how "market value" is calculated. There is no correct choice for this, as there is no such thing as "market value". There are some useful choices though:

  • Midpoint - A common choice is to take the mid-point of the bid-ask spread. This takes the top bid and ask prices of the order book (for a particular exchange) and averages them.
  • Last Traded Price - This is the last price that a stock traded at. Calculating this is tricky because the trade may have occurred across multiple prices (different lots at different prices). Hence it could be a weighted average.
  • Bid or Ask - Depending upon the side of the transaction (i.e. a purchase or sale), the top bid or ask price could be used as an indication.

None of these prices are likely to be the one achieved in practice, however. The order book dynamics, slippage and market impact are all going to cause the true sale price to differ from the currently quoted bid/ask.

In the following method I have opted for the midpoint calculation in order to provide some sense of "market value". It is important to stress, however, that this value can find its way into the risk management and position sizing calculations, so it is necessary to make sure that you are happy with how it is calculated and modify it accordingly if you wish to use it in your own trading engines.

Once the market value is calculated it allows subsequent calculation of the unrealised and realised PnL of the position:

# position.py

    def update_market_value(self, bid, ask):
        """
        The market value is tricky to calculate as we only have
        access to the top of the order book through Interactive
        Brokers, which means that the true redemption price is
        unknown until executed.
        However, it can be estimated via the mid-price of the
        bid-ask spread. Once the market value is calculated it
        allows calculation of the unrealised and realised profit
        and loss of any transactions.
        """
        midpoint = (bid+ask)/Decimal("2.0")
        self.market_value = (
            self.quantity * midpoint
        ).quantize(TWOPLACES)
        self.unrealised_pnl = (
            self.market_value - self.cost_basis
        ).quantize(TWOPLACES)
        self.realised_pnl = (
            self.market_value + self.net_incl_comm
        )

The final method is transact_shares. This is the method called by the Portfolio class to actually carry out a transaction. I won't repeat the method in full here, as it can be found above and at Github, but I will concentrate on some important sections.

In the code snippet below you can see that if the action is a purchase ("BOT") then the average buy price is recalculated. If the original action was also a purchase, the average price is necessarily modified. The total buys are increased by the new quantity and the total purchase price is modified. The logic is similar for the sell/"SLD" side:

# position.py

        if action == "BOT":
            self.avg_bot = (
                (self.avg_bot*self.buys + price*quantity)/(self.buys + quantity)
            ).quantize(FIVEPLACES)
            if self.action != "SLD":
                self.avg_price = (
                    (
                        self.avg_price*self.buys +
                        price*quantity+commission
                    )/(self.buys + quantity)
                ).quantize(FIVEPLACES)
            self.buys += quantity
            self.total_bot = (self.buys * self.avg_bot).quantize(TWOPLACES)

Finally, the net values are all adjusted. The calculations are relatively straightforward and can be followed from the snippet. Notice that they are all rounded to two decimal places:

# position.py

        # Adjust net values, including commissions
        self.net = self.buys - self.sells
        self.quantity = self.net
        self.net_total = (
            self.total_sld - self.total_bot
        ).quantize(TWOPLACES)
        self.net_incl_comm = (
            self.net_total - self.total_commission
        ).quantize(TWOPLACES)

        # Adjust average price and cost basis
        self.cost_basis = (
            self.quantity * self.avg_price
        ).quantize(TWOPLACES)

That concludes the Position class. It provides a robust mechanism for handling position calculation and storage.

For completeness you can find the full code for the Position class on Github at position.py.

position_test.py

In order to test the calculations within the Position class I've written the following unit tests, checking them against similar transactions carried out within Interactive Brokers. I do fully anticipate finding new edge cases and bugs that will need error correction, but these current unit tests will provide strong confidence in the results going forward.

The full listing of position_test.py is as follows:

from decimal import Decimal
import unittest

from qstrader.position.position import Position


class TestRoundTripXOMPosition(unittest.TestCase):
    """
    Test a round-trip trade in Exxon-Mobil where the initial
    trade is a buy/long of 100 shares of XOM, at a price of
    $74.78, with $1.00 commission.
    """
    def setUp(self):
        """
        Set up the Position object that will store the PnL.
        """
        self.position = Position(
            "BOT", "XOM", Decimal('100'), 
            Decimal("74.78"), Decimal("1.00"), 
            Decimal('74.78'), Decimal('74.80')
        )

    def test_calculate_round_trip(self):
        """
        After the subsequent purchase, carry out two more buys/longs
        and then close the position out with two additional sells/shorts.

        The following prices have been tested against those calculated
        via Interactive Brokers' Trader Workstation (TWS).
        """
        self.position.transact_shares(
            "BOT", Decimal('100'), Decimal('74.63'), Decimal('1.00')
        )
        self.position.transact_shares(
            "BOT", Decimal('250'), Decimal('74.620'), Decimal('1.25')
        )
        self.position.transact_shares(
            "SLD", Decimal('200'), Decimal('74.58'), Decimal('1.00')
        )
        self.position.transact_shares(
            "SLD", Decimal('250'), Decimal('75.26'), Decimal('1.25')
        )
        self.position.update_market_value(Decimal("77.75"), Decimal("77.77"))

        self.assertEqual(self.position.action, "BOT")
        self.assertEqual(self.position.ticker, "XOM")
        self.assertEqual(self.position.quantity, Decimal("0"))
        
        self.assertEqual(self.position.buys, Decimal("450"))
        self.assertEqual(self.position.sells, Decimal("450"))
        self.assertEqual(self.position.net, Decimal("0"))
        self.assertEqual(self.position.avg_bot, Decimal("74.65778"))
        self.assertEqual(self.position.avg_sld, Decimal("74.95778"))
        self.assertEqual(self.position.total_bot, Decimal("33596.00"))
        self.assertEqual(self.position.total_sld, Decimal("33731.00"))
        self.assertEqual(self.position.net_total, Decimal("135.00"))
        self.assertEqual(self.position.total_commission, Decimal("5.50"))
        self.assertEqual(self.position.net_incl_comm, Decimal("129.50"))

        self.assertEqual(self.position.avg_price, Decimal("74.665"))
        self.assertEqual(self.position.cost_basis, Decimal("0.00"))
        self.assertEqual(self.position.market_value, Decimal("0.00"))
        self.assertEqual(self.position.unrealised_pnl, Decimal("0.00"))
        self.assertEqual(self.position.realised_pnl, Decimal("129.50"))


class TestRoundTripPGPosition(unittest.TestCase):
    """
    Test a round-trip trade in Proctor & Gamble where the initial
    trade is a sell/short of 100 shares of PG, at a price of
    $77.69, with $1.00 commission.
    """
    def setUp(self):
        self.position = Position(
            "SLD", "PG", Decimal('100'), 
            Decimal("77.69"), Decimal("1.00"), 
            Decimal('77.68'), Decimal('77.70')
        )

    def test_calculate_round_trip(self):
        """
        After the subsequent sale, carry out two more sells/shorts
        and then close the position out with two additional buys/longs.

        The following prices have been tested against those calculated
        via Interactive Brokers' Trader Workstation (TWS).
        """
        self.position.transact_shares(
            "SLD", Decimal('100'), Decimal('77.68'), Decimal('1.00')
        )
        self.position.transact_shares(
            "SLD", Decimal('50'), Decimal('77.70'), Decimal('1.00')
        )
        self.position.transact_shares(
            "BOT", Decimal('100'), Decimal('77.77'), Decimal('1.00')
        )
        self.position.transact_shares(
            "BOT", Decimal('150'), Decimal('77.73'), Decimal('1.00')
        )
        self.position.update_market_value(Decimal("77.72"), Decimal("77.72"))

        self.assertEqual(self.position.action, "SLD")
        self.assertEqual(self.position.ticker, "PG")
        self.assertEqual(self.position.quantity, Decimal("0"))
        
        self.assertEqual(self.position.buys, Decimal("250"))
        self.assertEqual(self.position.sells, Decimal("250"))
        self.assertEqual(self.position.net, Decimal("0"))
        self.assertEqual(self.position.avg_bot, Decimal("77.746"))
        self.assertEqual(self.position.avg_sld, Decimal("77.688"))
        self.assertEqual(self.position.total_bot, Decimal("19436.50"))
        self.assertEqual(self.position.total_sld, Decimal("19422.00"))
        self.assertEqual(self.position.net_total, Decimal("-14.50"))
        self.assertEqual(self.position.total_commission, Decimal("5.00"))
        self.assertEqual(self.position.net_incl_comm, Decimal("-19.50"))

        self.assertEqual(self.position.avg_price, Decimal("77.67600"))
        self.assertEqual(self.position.cost_basis, Decimal("0.00"))
        self.assertEqual(self.position.market_value, Decimal("0.00"))
        self.assertEqual(self.position.unrealised_pnl, Decimal("0.00"))
        self.assertEqual(self.position.realised_pnl, Decimal("-19.50"))


if __name__ == "__main__":
    unittest.main()

The imports for this module are straightforward. We once again import the Decimal class, but also add the unittest module as well as the Position class itself, since it is being tested:

from decimal import Decimal
import unittest

from qstrader.position.position import Position

For those who have not yet seen a Python unit test, the basic idea is to create a class called TestXXXX that inherits from unittest.TestCase as in the subsequent snippet. The class exposes a setUp method that allows any data or state to be utilised for the remainder of that particular test. Here's an example unit test setup of a "round trip" trade for Exxon-Mobil/XOM:

class TestRoundTripXOMPosition(unittest.TestCase):
    """
    Test a round-trip trade in Exxon-Mobil where the initial
    trade is a buy/long of 100 shares of XOM, at a price of
    $74.78, with $1.00 commission.
    """
    def setUp(self):
        """
        Set up the Position object that will store the PnL.
        """
        self.position = Position(
            "BOT", "XOM", Decimal('100'), 
            Decimal("74.78"), Decimal("1.00"), 
            Decimal('74.78'), Decimal('74.80')
        )

Notice that self.position is set to be a new Position class, where 100 shares of XOM are purchased for 74.78 USD.

Subsequent methods, in the format of test_XXXX, allow unit testing of various aspects of the system. In this particular method, after the initial purchase, two more long purchases are made and finally two sales are made to net the position out to zero:

# position_test.py

    def test_calculate_round_trip(self):
        """
        After the subsequent purchase, carry out two more buys/longs
        and then close the position out with two additional sells/shorts.
        The following prices have been tested against those calculated
        via Interactive Brokers' Trader Workstation (TWS).
        """
        self.position.transact_shares(
            "BOT", Decimal('100'), Decimal('74.63'), Decimal('1.00')
        )
        self.position.transact_shares(
            "BOT", Decimal('250'), Decimal('74.620'), Decimal('1.25')
        )
        self.position.transact_shares(
            "SLD", Decimal('200'), Decimal('74.58'), Decimal('1.00')
        )
        self.position.transact_shares(
            "SLD", Decimal('250'), Decimal('75.26'), Decimal('1.25')
        )
        self.position.update_market_value(Decimal("77.75"), Decimal("77.77"))

transact_shares is called four times and finally the market value is updated by update_market_value. At this point self.position stores all of the various calculations and is ready to be tested, using the assertEqual method derived from the unittest.TestCase class. Notice how all of the various properties of the Position class are tested against externally calculated (in this case by Interactive Brokers TWS) values:

# position_test.py

        self.assertEqual(self.position.action, "BOT")
        self.assertEqual(self.position.ticker, "XOM")
        self.assertEqual(self.position.quantity, Decimal("0"))
        
        self.assertEqual(self.position.buys, Decimal("450"))
        self.assertEqual(self.position.sells, Decimal("450"))
        self.assertEqual(self.position.net, Decimal("0"))
        self.assertEqual(self.position.avg_bot, Decimal("74.65778"))
        self.assertEqual(self.position.avg_sld, Decimal("74.95778"))
        self.assertEqual(self.position.total_bot, Decimal("33596.00"))
        self.assertEqual(self.position.total_sld, Decimal("33731.00"))
        self.assertEqual(self.position.net_total, Decimal("135.00"))
        self.assertEqual(self.position.total_commission, Decimal("5.50"))
        self.assertEqual(self.position.net_incl_comm, Decimal("129.50"))

        self.assertEqual(self.position.avg_price, Decimal("74.665"))
        self.assertEqual(self.position.cost_basis, Decimal("0.00"))
        self.assertEqual(self.position.market_value, Decimal("0.00"))
        self.assertEqual(self.position.unrealised_pnl, Decimal("0.00"))
        self.assertEqual(self.position.realised_pnl, Decimal("129.50"))

When I execute the correct Python binary from within my virtual environment (under the command line in Ubuntu), I receive the following output:

(qstrader)mhallsmoore@desktop:~/sites/qstrader/approot/position$ python position_test.py

Ran 2 tests in 0.000s

OK

Note that there are two tests within the full file, so take a look at the second test in the full listing in order to familiarise yourself with the calculations.

Clearly there are a lot more tests that could be carried out on the Position class. At this stage we can be reassured that it handles the basic functionality of holding an equity position. In time, the class will likely be expanded to cope with forex, futures and options instruments, allowing a sophisticated investment strategy to be carried out within a portfolio.

The full listing itself can be found at position_test.py on Github.

Next Steps

In the following articles we are going to look at the Portfolio and PortfolioHandler classes. Both of these are currently coded up and unit tested in a basic manner. If you wish to "jump ahead" and see the code, you can take a look at the full QSTrader repository here: https://github.com/mhallsmoore/qstrader.