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.
1 Answer 1
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
...
namedtuple
s 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\$MarketOnClosePortfolio
will help us give more taillored advices. \$\endgroup\$