1#!/usr/bin/env python 2# -*- coding: utf-8; py-indent-offset:4 -*- 3############################################################################### 4# 5# Copyright (C) 2017 Daniel Rodriguez 6# 7# This program is free software: you can redistribute it and/or modify 8# it under the terms of the GNU General Public License as published by 9# the Free Software Foundation, either version 3 of the License, or 10# (at your option) any later version. 11# 12# This program is distributed in the hope that it will be useful, 13# but WITHOUT ANY WARRANTY; without even the implied warranty of 14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15# GNU General Public License for more details. 16# 17# You should have received a copy of the GNU General Public License 18# along with this program. If not, see <http://www.gnu.org/licenses/>. 19# 20############################################################################### 21from __future__ import (absolute_import, division, print_function, 22 unicode_literals) 23 24import argparse 25import datetime 26 27import backtrader as bt 28 29 30class BaseStrategy(bt.Strategy): 31 params = dict( 32 fast_ma=10, 33 slow_ma=20, 34 ) 35 36 def __init__(self): 37 # omitting a data implies self.datas[0] (aka self.data and self.data0) 38 fast_ma = bt.ind.EMA(period=self.p.fast_ma) 39 slow_ma = bt.ind.EMA(period=self.p.slow_ma) 40 # our entry point 41 self.crossup = bt.ind.CrossUp(fast_ma, slow_ma) 42 43 44class ManualStopOrStopTrail(BaseStrategy): 45 params = dict( 46 stop_loss=0.02, # price is 2% less than the entry point 47 trail=False, 48 ) 49 50 def notify_order(self, order): 51 if not order.status == order.Completed: 52 return # discard any other notification 53 54 if not self.position: # we left the market 55 print('SELL@price: {:.2f}'.format(order.executed.price)) 56 return 57 58 # We have entered the market 59 print('BUY @price: {:.2f}'.format(order.executed.price)) 60 61 if not self.p.trail: 62 stop_price = order.executed.price * (1.0 - self.p.stop_loss) 63 self.sell(exectype=bt.Order.Stop, price=stop_price) 64 else: 65 self.sell(exectype=bt.Order.StopTrail, trailamount=self.p.trail) 66 67 def next(self): 68 if not self.position and self.crossup > 0: 69 # not in the market and signal triggered 70 self.buy() 71 72 73class ManualStopOrStopTrailCheat(BaseStrategy): 74 params = dict( 75 stop_loss=0.02, # price is 2% less than the entry point 76 trail=False, 77 ) 78 79 def __init__(self): 80 super().__init__() 81 self.broker.set_coc(True) 82 83 def notify_order(self, order): 84 if not order.status == order.Completed: 85 return # discard any other notification 86 87 if not self.position: # we left the market 88 print('SELL@price: {:.2f}'.format(order.executed.price)) 89 return 90 91 # We have entered the market 92 print('BUY @price: {:.2f}'.format(order.executed.price)) 93 94 def next(self): 95 if not self.position and self.crossup > 0: 96 # not in the market and signal triggered 97 self.buy() 98 99 if not self.p.trail: 100 stop_price = self.data.close[0] * (1.0 - self.p.stop_loss) 101 self.sell(exectype=bt.Order.Stop, price=stop_price) 102 else: 103 self.sell(exectype=bt.Order.StopTrail, 104 trailamount=self.p.trail) 105 106 107class AutoStopOrStopTrail(BaseStrategy): 108 params = dict( 109 stop_loss=0.02, # price is 2% less than the entry point 110 trail=False, 111 buy_limit=False, 112 ) 113 114 buy_order = None # default value for a potential buy_order 115 116 def notify_order(self, order): 117 if order.status == order.Cancelled: 118 print('CANCEL@price: {:.2f} {}'.format( 119 order.executed.price, 'buy' if order.isbuy() else 'sell')) 120 return 121 122 if not order.status == order.Completed: 123 return # discard any other notification 124 125 if not self.position: # we left the market 126 print('SELL@price: {:.2f}'.format(order.executed.price)) 127 return 128 129 # We have entered the market 130 print('BUY @price: {:.2f}'.format(order.executed.price)) 131 132 def next(self): 133 if not self.position and self.crossup > 0: 134 if self.buy_order: # something was pending 135 self.cancel(self.buy_order) 136 137 # not in the market and signal triggered 138 if not self.p.buy_limit: 139 self.buy_order = self.buy(transmit=False) 140 else: 141 price = self.data.close[0] * (1.0 - self.p.buy_limit) 142 143 # transmit = False ... await child order before transmission 144 self.buy_order = self.buy(price=price, exectype=bt.Order.Limit, 145 transmit=False) 146 147 # Setting parent=buy_order ... sends both together 148 if not self.p.trail: 149 stop_price = self.data.close[0] * (1.0 - self.p.stop_loss) 150 self.sell(exectype=bt.Order.Stop, price=stop_price, 151 parent=self.buy_order) 152 else: 153 self.sell(exectype=bt.Order.StopTrail, 154 trailamount=self.p.trail, 155 parent=self.buy_order) 156 157 158APPROACHES = dict( 159 manual=ManualStopOrStopTrail, 160 manualcheat=ManualStopOrStopTrailCheat, 161 auto=AutoStopOrStopTrail, 162) 163 164 165def runstrat(args=None): 166 args = parse_args(args) 167 168 cerebro = bt.Cerebro() 169 170 # Data feed kwargs 171 kwargs = dict() 172 173 # Parse from/to-date 174 dtfmt, tmfmt = '%Y-%m-%d', 'T%H:%M:%S' 175 for a, d in ((getattr(args, x), x) for x in ['fromdate', 'todate']): 176 if a: 177 strpfmt = dtfmt + tmfmt * ('T' in a) 178 kwargs[d] = datetime.datetime.strptime(a, strpfmt) 179 180 data0 = bt.feeds.BacktraderCSVData(dataname=args.data0, **kwargs) 181 cerebro.adddata(data0) 182 183 # Broker 184 cerebro.broker = bt.brokers.BackBroker(**eval('dict(' + args.broker + ')')) 185 186 # Sizer 187 cerebro.addsizer(bt.sizers.FixedSize, **eval('dict(' + args.sizer + ')')) 188 189 # Strategy 190 StClass = APPROACHES[args.approach] 191 cerebro.addstrategy(StClass, **eval('dict(' + args.strat + ')')) 192 193 # Execute 194 cerebro.run(**eval('dict(' + args.cerebro + ')')) 195 196 if args.plot: # Plot if requested to 197 cerebro.plot(**eval('dict(' + args.plot + ')')) 198 199 200def parse_args(pargs=None): 201 parser = argparse.ArgumentParser( 202 formatter_class=argparse.ArgumentDefaultsHelpFormatter, 203 description=( 204 'Stop-Loss Approaches' 205 ) 206 ) 207 208 parser.add_argument('--data0', default='../../datas/2005-2006-day-001.txt', 209 required=False, help='Data to read in') 210 211 # Strategy to choose 212 parser.add_argument('approach', choices=APPROACHES.keys(), 213 help='Stop approach to use') 214 215 # Defaults for dates 216 parser.add_argument('--fromdate', required=False, default='', 217 help='Date[time] in YYYY-MM-DD[THH:MM:SS] format') 218 219 parser.add_argument('--todate', required=False, default='', 220 help='Date[time] in YYYY-MM-DD[THH:MM:SS] format') 221 222 parser.add_argument('--cerebro', required=False, default='', 223 metavar='kwargs', help='kwargs in key=value format') 224 225 parser.add_argument('--broker', required=False, default='', 226 metavar='kwargs', help='kwargs in key=value format') 227 228 parser.add_argument('--sizer', required=False, default='', 229 metavar='kwargs', help='kwargs in key=value format') 230 231 parser.add_argument('--strat', required=False, default='', 232 metavar='kwargs', help='kwargs in key=value format') 233 234 parser.add_argument('--plot', required=False, default='', 235 nargs='?', const='{}', 236 metavar='kwargs', help='kwargs in key=value format') 237 238 return parser.parse_args(pargs) 239 240 241if __name__ == '__main__': 242 runstrat() 243