3
\$\begingroup\$

I simply want to organize some data (class= MarketOnCloseSecurity) to be used in another class MarketOnClosePortfolio. The former class would not have any methods but just a constructor that includes a ticker for a company symbol and two Pandas dataframes bars and signals.

class MarketOnCloseSecurity():
    """Encapsulates the notion of a portfolio of positions based
    on a set of signals as provided by a Strategy.

    Requires:
    symbol - A stock symbol which forms the basis of the portfolio.
    bars - A DataFrame of bars for a symbol set.
    signals - A pandas DataFrame of signals (1, 0, -1) for each symbol.
   """

    def __init__(self, symbol, bars, signals):
        self.symbol = symbol
        self.bars = bars
        self.signals = signals

Now the MarketOnClosePortfolio would have all the methods to calculate a portfolio.

Is creating a class with essentially no methods (besides constructor) in python a good practice?

The idea is to pass MarketOnClosePortfolio a list of MarketOnCloseSecurity objects.

An alternative (non-OOP) solution would do something like list_of_MOCS = [{'symbol_1':(bars_1,signals_1)},...,{'symbol_N':(bars_N,signals_N)}] so basically a list containing a map with the key being the ticker and value being a tuple of the two dataframes.

MarketOnClosePortfolio will identify the positions (buy/sell) and how based on each MarketOnCloseSecurity and form a Portfolio Dataframe. It will then calculate metrics, such as holdings, cash, and returns based on the summation of each MarketOnCloseSecurity. The code is not complete, but I have included it for reference.

class MarketOnClosePortfolio(Portfolio):
    """Encapsulates the notion of a portfolio of positions based
    on a set of signals as provided by a Strategy.

    Requires:
    market_on_close_securities
    initial_capital - The amount in cash at the start of the portfolio."""

    def __init__(self, market_on_close_securities, initial_capital=100000.0):
        self.market_on_close_securities = market_on_close_securities
        self.initial_capital = float(initial_capital)
        self.positions = self.generate_positions()

    def generate_positions(self):
        #loop through each MOCS (market_on_close_security) to calculate positions

        # Initialize index for positions Dataframe. Picking index 0 was arbitary they all have same index length
        positions = pd.DataFrame(index=self.market_on_close_securities[0].signals.index).fillna(0.0)
        for security in self.market_on_close_securities:
            positions[security.symbol] = 100 * security.signals['signal'] # This strategy buys 100 shares

        return positions

    def backtest_portfolio(self):

        portfolio_series = self.positions[self.symbol] * self.bars[0]['close_price'].astype(float)

        pos_diff = self.positions.diff()

        # Will need to use this for multiple columns (Eventually)
        symbol_columns = [self.symbol]

        portfolio = pd.DataFrame(index=self.bars[0].index, columns=symbol_columns)
        portfolio[self.symbol] = portfolio_series.values

        # Sum holdings from each company
        portfolio['holdings'] = portfolio.sum(axis=1)
        portfolio['cash'] = self.initial_capital - (pos_diff[self.symbol] * self.bars[0]['close_price'].astype(float)).cumsum()
        portfolio['cash'][0] = self.initial_capital
        portfolio['total'] = portfolio['cash'] + portfolio['holdings']
        portfolio['returns'] = portfolio['total'].pct_change()

        return portfolio

The backtest_portfolio() will determine the total returns/holdings based on each MarketOnCloseSecurity. This implementation is not complete, and is currently the method used when MarketOnClosePortfolio was adapted for only one security.

\$\endgroup\$
  • 2
    \$\begingroup\$ I think it's ok. But, most of time, i use NamedTuple for POJO like objects. Not sure that it is possible with Dataframes. \$\endgroup\$ – Arnial Oct 27 '16 at 17:49
  • \$\begingroup\$ namedtuples are awesome. You can inherit from them too in cases where you don't need to alter them just a tiny bit - like this \$\endgroup\$ – Daerdemandt Oct 27 '16 at 18:01
  • \$\begingroup\$ Knowing how you would use this data into MarketOnClosePortfolio will help us give more taillored advices. \$\endgroup\$ – 301_Moved_Permanently Oct 27 '16 at 19:32
  • \$\begingroup\$ Updated with what I have so far. Let me know what you think! Also thanks for the input so far from everyone. \$\endgroup\$ – user3547551 Oct 27 '16 at 19:42
2
\$\begingroup\$

When reading

An alternative (non-OOP) solution would do something like list_of_MOCS = [{'symbol_1':(bars_1,signals_1)},...,{'symbol_N':(bars_N,signals_N)}] so basically a list containing a map with the key being the ticker and value being a tuple of the two dataframes.

I thought, that a better alternative would be:

MOCS = {
    'symbol_1': (bars_1, signals_1),
    'symbol_2': (bars_2, signals_2),
    ...
    'symbol_N': (bars_N, signals_N),
}

Especially given the fact that pd.DataFrame(MOCS) would yield a valid structure.

However, reading at your usage in generate_positions and expected usage in backtest_portfolio; and especially the fact that you will need to explode the data-structure anyway, I would keep the class approach and use collections.namedtuple as largely suggested in the comments. It provides both the advantages of a class (attributes access) and a tuple (immutable + index access).

I would however change generate_positions to create a dictionnary that will hold the structure of the resulting dataframe before building said dataframe. And incorporate that method directly into the constructor as it does not provide much added value on its own:

from collections import namedtuple


MarketOnCloseSecurity = namedtuple('MarketOnCloseSecurity', 'symbol bars signals')


class MarketOnClosePortfolio(Portfolio):
    """Encapsulates the notion of a portfolio of positions based
    on a set of signals as provided by a Strategy.

    Requires:
    market_on_close_securities
    initial_capital - The amount in cash at the start of the portfolio."""

    def __init__(self, market_on_close_securities, initial_capital=100000.0):
        self.market_on_close_securities = market_on_close_securities
        self.initial_capital = float(initial_capital)

        #loop through each MOCS (market_on_close_security) to calculate positions
        securities = {
            security.symbol: 100 * security.signals['signal'] # This strategy buys 100 shares
            for security in market_on_close_securities
        }
        self.positions = pd.DataFrame(securities)

    def backtest_portfolio(self):
        ...

I would also extend the namedtuple to provide extra utilities methods:

class MarketOnCloseSecurity(namedtuple('MarketOnCloseSecurity', 'symbol bars signals')):
    def buy_shares(self, amount):
        return amount * self.signals['signal']

    def close_price(self):
        return self.bars['close_price'].astype(float)

So that you can have more meaningful actions in MarketOnClosePortfolio:

class MarketOnClosePortfolio(Portfolio):
    """Encapsulates the notion of a portfolio of positions based
    on a set of signals as provided by a Strategy.

    Requires:
    market_on_close_securities
    initial_capital - The amount in cash at the start of the portfolio."""

    def __init__(self, market_on_close_securities, initial_capital=100000.0):
        self.market_on_close_securities = market_on_close_securities
        self.initial_capital = float(initial_capital)

        securities = {
            security.symbol: security.buy_shares(100)
            for security in market_on_close_securities
        }
        self.positions = pd.DataFrame(securities)

    def backtest_portfolio(self):
        # use security.close_price() for any security in self.marke_on_close_securities if you need to
        ...
| improve this answer | |
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.