from moonshot import Moonshot
from moonshot.commission import PerShareCommission
class UpMinusDown(Moonshot):
"""
Strategy that buys recent winners and sells recent losers.
Specifically:
- rank stocks by their performance over the past MOMENTUM_WINDOW days
- ignore very recent performance by excluding the last RANKING_PERIOD_GAP
days from the ranking window (as commonly recommended for UMD)
- buy the TOP_N_PCT percent of highest performing stocks and short the TOP_N_PCT
percent of lowest performing stocks
- rebalance the portfolio according to REBALANCE_INTERVAL
"""
CODE = "umd"
MOMENTUM_WINDOW = 252
RANKING_PERIOD_GAP = 22
TOP_N_PCT = 50
REBALANCE_INTERVAL = "M"
def prices_to_signals(self, prices):
"""
This method receives a DataFrame of prices and should return a
DataFrame of integer signals, where 1=long, -1=short, and 0=cash.
"""
closes = prices.loc["Close"]
returns = closes.shift(self.RANKING_PERIOD_GAP)/closes.shift(self.MOMENTUM_WINDOW) - 1
top_ranks = returns.rank(axis=1, ascending=False, pct=True)
bottom_ranks = returns.rank(axis=1, ascending=True, pct=True)
top_n_pct = self.TOP_N_PCT / 100
longs = (top_ranks <= top_n_pct)
shorts = (bottom_ranks <= top_n_pct)
longs = longs.astype(int)
shorts = -shorts.astype(int)
signals = longs.where(longs == 1, shorts)
signals = signals.resample(self.REBALANCE_INTERVAL).last()
signals = signals.reindex(closes.index, method="ffill")
return signals
def signals_to_target_weights(self, signals, prices):
"""
This method receives a DataFrame of integer signals (-1, 0, 1) and
should return a DataFrame indicating how much capital to allocate to
the signals, expressed as a percentage of the total capital allocated
to the strategy (for example, -0.25, 0, 0.1 to indicate 25% short,
cash, 10% long).
"""
weights = self.allocate_equal_weights(signals)
return weights
def target_weights_to_positions(self, weights, prices):
"""
This method receives a DataFrame of allocations and should return a
DataFrame of positions. This allows for modeling the delay between
when the signal occurs and when the position is entered, and can also
be used to model non-fills.
"""
return weights.shift()
def positions_to_gross_returns(self, positions, prices):
"""
This method receives a DataFrame of positions and a DataFrame of
prices, and should return a DataFrame of percentage returns before
commissions and slippage.
"""
opens = prices.loc["Open"]
gross_returns = opens.pct_change() * positions.shift()
return gross_returns
class USStockCommission(PerShareCommission):
BROKER_COMMISSION_PER_SHARE = 0.005
class UpMinusDownDemo(UpMinusDown):
CODE = "umd-demo"
DB = "usstock-free-1d"
UNIVERSES = "usstock-free"
TOP_N_PCT = 50
COMMISSION_CLASS = USStockCommission