import backtrader as bt
import backtrader.feeds as btfeeds
import backtrader.indicators as btind
class CommInfoFloat(bt.CommInfoBase):
"""Commission schema that keeps size as float."""
params = (
('stocklike', True),
('commtype', bt.CommInfoBase.COMM_PERC),
('percabs', True),
)
def getsize(self, price, cash):
if not self._stocklike:
return self.p.leverage * (cash / self.get_margin(price))
return self.p.leverage * (cash / price)
class OLSSlopeIntercept(btind.PeriodN):
"""Calculates a linear regression using OLS."""
_mindatas = 2
packages = (
('pandas', 'pd'),
('statsmodels.api', 'sm'),
)
lines = ('slope', 'intercept',)
params = (
('period', 10),
)
def next(self):
p0 = pd.Series(self.data0.get(size=self.p.period))
p1 = pd.Series(self.data1.get(size=self.p.period))
p1 = sm.add_constant(p1)
intercept, slope = sm.OLS(p0, p1).fit().params
self.lines.slope[0] = slope
self.lines.intercept[0] = intercept
class Log(btind.Indicator):
"""Calculates log."""
lines = ('log',)
def next(self):
self.l.log[0] = math.log(self.data[0])
class OLSSpread(btind.PeriodN):
"""Calculates the z-score of the OLS spread."""
_mindatas = 2
lines = ('spread', 'spread_mean', 'spread_std', 'zscore',)
params = (('period', 10),)
def __init__(self):
data0_log = Log(self.data0)
data1_log = Log(self.data1)
slint = OLSSlopeIntercept(data0_log, data1_log, period=self.p.period)
spread = data0_log - (slint.slope * data1_log + slint.intercept)
self.l.spread = spread
self.l.spread_mean = bt.ind.SMA(spread, period=self.p.period)
self.l.spread_std = bt.ind.StdDev(spread, period=self.p.period)
self.l.zscore = (spread - self.l.spread_mean) / self.l.spread_std
class LogReturns(btind.PeriodN):
"""Calculates the log returns."""
lines = ('logret',)
params = (('period', 1),)
def __init__(self):
self.addminperiod(self.p.period + 1)
def next(self):
self.l.logret[0] = math.log(self.data[0] / self.data[-self.p.period])
class LogReturnSpread(btind.PeriodN):
"""Calculates the spread of the log returns."""
_mindatas = 2
lines = ('logret0', 'logret1', 'spread', 'spread_mean', 'spread_std', 'zscore',)
params = (('period', 10),)
def __init__(self):
self.l.logret0 = LogReturns(self.data0, period=1)
self.l.logret1 = LogReturns(self.data1, period=1)
self.l.spread = self.l.logret0 - self.l.logret1
self.l.spread_mean = bt.ind.SMA(self.l.spread, period=self.p.period)
self.l.spread_std = bt.ind.StdDev(self.l.spread, period=self.p.period)
self.l.zscore = (self.l.spread - self.l.spread_mean) / self.l.spread_std
class PairTradingStrategy(bt.Strategy):
"""Basic pair trading strategy."""
params = dict(
period=PERIOD,
order_pct1=ORDER_PCT1,
order_pct2=ORDER_PCT2,
printout=True,
upper=UPPER,
lower=LOWER,
mode=MODE
)
def log(self, txt, dt=None):
if self.p.printout:
dt = dt or self.data.datetime[0]
dt = bt.num2date(dt)
print('%s, %s' % (dt.isoformat(), txt))
def notify_order(self, order):
if order.status in [bt.Order.Submitted, bt.Order.Accepted]:
return
if order.status == order.Completed:
if order.isbuy():
buytxt = 'BUY COMPLETE {}, size = {:.2f}, price = {:.2f}'.format(
order.data._name, order.executed.size, order.executed.price)
self.log(buytxt, order.executed.dt)
else:
selltxt = 'SELL COMPLETE {}, size = {:.2f}, price = {:.2f}'.format(
order.data._name, order.executed.size, order.executed.price)
self.log(selltxt, order.executed.dt)
elif order.status in [order.Expired, order.Canceled, order.Margin]:
self.log('%s ,' % order.Status[order.status])
pass
self.orderid = None
def __init__(self):
self.orderid = None
self.order_pct1 = self.p.order_pct1
self.order_pct2 = self.p.order_pct2
self.upper = self.p.upper
self.lower = self.p.lower
self.status = 0
if self.p.mode == 'log_return':
self.transform = LogReturnSpread(self.data0, self.data1, period=self.p.period)
elif self.p.mode == 'OLS':
self.transform = OLSSpread(self.data0, self.data1, period=self.p.period)
else:
raise ValueError("Unknown mode")
self.spread = self.transform.spread
self.zscore = self.transform.zscore
self.spread_sr = pd.Series(dtype=float, name='spread')
self.zscore_sr = pd.Series(dtype=float, name='zscore')
self.short_signal_sr = pd.Series(dtype=bool, name='short_signals')
self.long_signal_sr = pd.Series(dtype=bool, name='long_signals')
def next(self):
if self.orderid:
return
self.spread_sr[self.data0.datetime.datetime()] = self.spread[0]
self.zscore_sr[self.data0.datetime.datetime()] = self.zscore[0]
self.short_signal_sr[self.data0.datetime.datetime()] = False
self.long_signal_sr[self.data0.datetime.datetime()] = False
if self.zscore[0] > self.upper and self.status != 1:
self.short_signal_sr[self.data0.datetime.datetime()] = True
self.log('SELL CREATE {}, price = {:.2f}, target pct = {:.2%}'.format(
self.data0._name, self.data0.close[0], -self.order_pct1))
self.order_target_percent(data=self.data0, target=-self.order_pct1)
self.log('BUY CREATE {}, price = {:.2f}, target pct = {:.2%}'.format(
self.data1._name, self.data1.close[0], self.order_pct2))
self.order_target_percent(data=self.data1, target=self.order_pct2)
self.status = 1
elif self.zscore[0] < self.lower and self.status != 2:
self.long_signal_sr[self.data0.datetime.datetime()] = True
self.log('SELL CREATE {}, price = {:.2f}, target pct = {:.2%}'.format(
self.data1._name, self.data1.close[0], -self.order_pct2))
self.order_target_percent(data=self.data1, target=-self.order_pct2)
self.log('BUY CREATE {}, price = {:.2f}, target pct = {:.2%}'.format(
self.data0._name, self.data0.close[0], self.order_pct1))
self.order_target_percent(data=self.data0, target=self.order_pct1)
self.status = 2
def stop(self):
if self.p.printout:
print('==================================================')
print('Starting Value - %.2f' % self.broker.startingcash)
print('Ending Value - %.2f' % self.broker.getvalue())
print('==================================================')