1#!/usr/bin/env python
2# -*- coding: utf-8; py-indent-offset:4 -*-
3###############################################################################
4#
5# Copyright (C) 2015, 2016 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
27# The above could be sent to an independent module
28import backtrader as bt
29import backtrader.feeds as btfeeds
30import backtrader.indicators as btind
31
32
33class MultiDataStrategy(bt.Strategy):
34    '''
35    This strategy operates on 2 datas. The expectation is that the 2 datas are
36    correlated and the 2nd data is used to generate signals on the 1st
37
38      - Buy/Sell Operationss will be executed on the 1st data
39      - The signals are generated using a Simple Moving Average on the 2nd data
40        when the close price crosses upwwards/downwards
41
42    The strategy is a long-only strategy
43    '''
44    params = dict(
45        period=15,
46        stake=10,
47        printout=True,
48    )
49
50    def log(self, txt, dt=None):
51        if self.p.printout:
52            dt = dt or self.data.datetime[0]
53            dt = bt.num2date(dt)
54            print('%s, %s' % (dt.isoformat(), txt))
55
56    def notify_order(self, order):
57        if order.status in [bt.Order.Submitted, bt.Order.Accepted]:
58            return  # Await further notifications
59
60        if order.status == order.Completed:
61            if order.isbuy():
62                buytxt = 'BUY COMPLETE, %.2f' % order.executed.price
63                self.log(buytxt, order.executed.dt)
64            else:
65                selltxt = 'SELL COMPLETE, %.2f' % order.executed.price
66                self.log(selltxt, order.executed.dt)
67
68        elif order.status in [order.Expired, order.Canceled, order.Margin]:
69            self.log('%s ,' % order.Status[order.status])
70            pass  # Simply log
71
72        # Allow new orders
73        self.orderid = None
74
75    def __init__(self):
76        # To control operation entries
77        self.orderid = None
78
79        # Create SMA on 2nd data
80        sma = btind.MovAv.SMA(self.data1, period=self.p.period)
81        # Create a CrossOver Signal from close an moving average
82        self.signal = btind.CrossOver(self.data1.close, sma)
83
84    def next(self):
85        if self.orderid:
86            return  # if an order is active, no new orders are allowed
87
88        if self.p.printout:
89            print('Self  len:', len(self))
90            print('Data0 len:', len(self.data0))
91            print('Data1 len:', len(self.data1))
92            print('Data0 len == Data1 len:',
93                  len(self.data0) == len(self.data1))
94
95            print('Data0 dt:', self.data0.datetime.datetime())
96            print('Data1 dt:', self.data1.datetime.datetime())
97
98        if not self.position:  # not yet in market
99            if self.signal > 0.0:  # cross upwards
100                self.log('BUY CREATE , %.2f' % self.data1.close[0])
101                self.buy(size=self.p.stake)
102
103        else:  # in the market
104            if self.signal < 0.0:  # crosss downwards
105                self.log('SELL CREATE , %.2f' % self.data1.close[0])
106                self.sell(size=self.p.stake)
107
108    def stop(self):
109        print('==================================================')
110        print('Starting Value - %.2f' % self.broker.startingcash)
111        print('Ending   Value - %.2f' % self.broker.getvalue())
112        print('==================================================')
113
114
115def runstrategy():
116    args = parse_args()
117
118    # Create a cerebro
119    cerebro = bt.Cerebro()
120
121    # Get the dates from the args
122    fromdate = datetime.datetime.strptime(args.fromdate, '%Y-%m-%d')
123    todate = datetime.datetime.strptime(args.todate, '%Y-%m-%d')
124
125    # Create the 1st data
126    data0 = btfeeds.YahooFinanceCSVData(
127        dataname=args.data0,
128        fromdate=fromdate,
129        todate=todate)
130
131    # Add the 1st data to cerebro
132    cerebro.adddata(data0)
133
134    # Create the 2nd data
135    data1 = btfeeds.YahooFinanceCSVData(
136        dataname=args.data1,
137        fromdate=fromdate,
138        todate=todate)
139
140    # Add the 2nd data to cerebro
141    cerebro.adddata(data1)
142
143    # Add the strategy
144    cerebro.addstrategy(MultiDataStrategy,
145                        period=args.period,
146                        stake=args.stake)
147
148    # Add the commission - only stocks like a for each operation
149    cerebro.broker.setcash(args.cash)
150
151    # Add the commission - only stocks like a for each operation
152    cerebro.broker.setcommission(commission=args.commperc)
153
154    # And run it
155    cerebro.run(runonce=not args.runnext,
156                preload=not args.nopreload,
157                oldsync=args.oldsync)
158
159    # Plot if requested
160    if args.plot:
161        cerebro.plot(numfigs=args.numfigs, volume=False, zdown=False)
162
163
164def parse_args():
165    parser = argparse.ArgumentParser(description='MultiData Strategy')
166
167    parser.add_argument('--data0', '-d0',
168                        default='../../datas/orcl-2003-2005.txt',
169                        help='1st data into the system')
170
171    parser.add_argument('--data1', '-d1',
172                        default='../../datas/yhoo-2003-2005.txt',
173                        help='2nd data into the system')
174
175    parser.add_argument('--fromdate', '-f',
176                        default='2003-01-01',
177                        help='Starting date in YYYY-MM-DD format')
178
179    parser.add_argument('--todate', '-t',
180                        default='2005-12-31',
181                        help='Starting date in YYYY-MM-DD format')
182
183    parser.add_argument('--period', default=15, type=int,
184                        help='Period to apply to the Simple Moving Average')
185
186    parser.add_argument('--cash', default=100000, type=int,
187                        help='Starting Cash')
188
189    parser.add_argument('--runnext', action='store_true',
190                        help='Use next by next instead of runonce')
191
192    parser.add_argument('--nopreload', action='store_true',
193                        help='Do not preload the data')
194
195    parser.add_argument('--oldsync', action='store_true',
196                        help='Use old data synchronization method')
197
198    parser.add_argument('--commperc', default=0.005, type=float,
199                        help='Percentage commission (0.005 is 0.5%%')
200
201    parser.add_argument('--stake', default=10, type=int,
202                        help='Stake to apply in each operation')
203
204    parser.add_argument('--plot', '-p', action='store_true',
205                        help='Plot the read data')
206
207    parser.add_argument('--numfigs', '-n', default=1,
208                        help='Plot using numfigs figures')
209
210    return parser.parse_args()
211
212
213if __name__ == '__main__':
214    runstrategy()
215