import pandas as pd
from statsmodels.tsa.vector_ar.vecm import coint_johansen
from moonshot import Moonshot
from moonshot.commission import PerShareCommission
class USStockCommission(PerShareCommission):
BROKER_COMMISSION_PER_SHARE = 0.005
class PairsStrategy(Moonshot):
"""
Pairs trading strategy that uses the Johansen test to re-calculate
hedge ratios daily and uses Bollinger Bands to time entries and exits.
Buys (sells) the spread when it crosses below (above) its lower (upper)
Bollinger Band and exits when it crosses its moving average.
To use the strategy, subclass this base class and define the appropriate
DB and SIDS.
"""
CODE = "pairs"
DB = None
DB_FIELDS = ["Close", "Open"]
SIDS = []
LOOKBACK_WINDOW = 20
BBAND_STD = 1
COMMISSION_CLASS = USStockCommission
def get_hedge_ratio(self, pair_prices: pd.DataFrame):
"""
Helper function that uses the Johansen test to calculate hedge ratio. This is applied
to the pair prices on a rolling basis in prices_to_signals.
"""
pair_prices = pair_prices.dropna()
if len(pair_prices) < self.LOOKBACK_WINDOW * 0.75:
return pd.Series(0, index=pair_prices.columns)
result = coint_johansen(pair_prices, 0, 1)
weights = list(result.evec[0])
return pd.Series(weights, index=pair_prices.columns)
def prices_to_signals(self, prices: pd.DataFrame):
"""
Generates a DataFrame of signals indicating whether to long or short the
spread.
"""
closes = prices.loc["Close"]
all_hedge_ratios = []
for idx in range(len(closes)):
start_idx = idx - self.LOOKBACK_WINDOW
some_closes = closes.iloc[start_idx:idx]
hedge_ratio = self.get_hedge_ratio(some_closes)
hedge_ratio = pd.Series(hedge_ratio).to_frame().T
all_hedge_ratios.append(hedge_ratio)
hedge_ratios = pd.concat(all_hedge_ratios)
hedge_ratios.index = closes.index
spreads = (closes * hedge_ratios).sum(axis=1)
means = spreads.fillna(method="ffill").rolling(self.LOOKBACK_WINDOW).mean()
stds = spreads.fillna(method="ffill").rolling(self.LOOKBACK_WINDOW).std()
upper_bands = means + self.BBAND_STD * stds
lower_bands = means - self.BBAND_STD * stds
long_entries = spreads < lower_bands
long_exits = spreads >= means
short_entries = spreads > upper_bands
short_exits = spreads <= means
ones = pd.Series(1, index=spreads.index)
zeros = pd.Series(0, index=spreads.index)
minus_ones = pd.Series(-1, index=spreads.index)
long_signals = ones.where(long_entries).fillna(zeros.where(long_exits)).fillna(method="ffill")
short_signals = minus_ones.where(short_entries).fillna(zeros.where(short_exits)).fillna(method="ffill")
signals = long_signals + short_signals
signals = closes.apply(lambda x: signals)
self.hedge_ratios = hedge_ratios
return signals
def signals_to_target_weights(self, signals: pd.DataFrame, prices: pd.DataFrame):
"""
Converts the DataFrame of integer signals, indicating whether to long
or short the spread, into the corresponding weight of each instrument
to hold.
"""
hedge_ratio_weights = self.hedge_ratios * prices.loc["Close"]
weights = signals * hedge_ratio_weights
total_weights= weights.abs().sum(axis=1)
weights = weights.div(total_weights, axis=0)
return weights
def target_weights_to_positions(self, weights: pd.DataFrame, prices: pd.DataFrame):
positions = weights.shift()
return positions
def positions_to_gross_returns(self, positions: pd.DataFrame, prices: pd.DataFrame):
opens = prices.loc["Open"]
gross_returns = opens.pct_change() * positions.shift()
return gross_returns
class GDX_GLD_Pair(PairsStrategy):
CODE = "pairs-gdx-gld"
DB = "usstock-1d"
SIDS = [
"FIBBG000PLNQN7",
"FIBBG000CRF6Q8",
]