493 lines
18 KiB
Python
493 lines
18 KiB
Python
from __future__ import (absolute_import, division, print_function,
|
|
unicode_literals)
|
|
|
|
import argparse
|
|
import datetime
|
|
import sys
|
|
|
|
# The above could be sent to an independent module
|
|
import backtrader as bt
|
|
from backtrader.utils import flushfile # win32 quick stdout flushing
|
|
|
|
from mql5_zmq_backtrader.mt5data import MTraderData
|
|
from mql5_zmq_backtrader.mt5store import MTraderStore
|
|
|
|
StoreCls = MTraderStore
|
|
DataCls = MTraderData
|
|
|
|
|
|
class TestStrategy(bt.Strategy):
|
|
params = dict(
|
|
smaperiod=5,
|
|
trade=False,
|
|
stake=0.1,
|
|
exectype=bt.Order.Market,
|
|
stopafter=0,
|
|
valid=None,
|
|
cancel=0,
|
|
donotcounter=False,
|
|
sell=False,
|
|
usebracket=False,
|
|
)
|
|
|
|
def __init__(self):
|
|
# To control operation entries
|
|
self.orderid = list()
|
|
self.order = None
|
|
|
|
self.counttostop = 0
|
|
self.datastatus = 0
|
|
|
|
self.last_pos = None
|
|
self.last_value = 0
|
|
|
|
# Create SMA on 2nd data
|
|
self.sma = bt.indicators.MovAv.SMA(self.data, period=self.p.smaperiod)
|
|
|
|
print('--------------------------------------------------')
|
|
print('Strategy Created')
|
|
print('--------------------------------------------------')
|
|
|
|
def notify_fund(self, cash, value, fundvalue, shares):
|
|
if value != self.last_value:
|
|
print(cash, value, fundvalue, shares)
|
|
self.last_value = value
|
|
|
|
def notify_data(self, data, status, *args, **kwargs):
|
|
print('*' * 5, 'DATA NOTIF:', data._getstatusname(status), *args)
|
|
if status == data.LIVE:
|
|
self.counttostop = self.p.stopafter
|
|
self.datastatus = 1
|
|
|
|
def notify_store(self, msg, *args, **kwargs):
|
|
print('*' * 5, 'STORE NOTIF:', msg)
|
|
|
|
def notify_order(self, order):
|
|
if order.status in [order.Completed, order.Cancelled, order.Rejected]:
|
|
self.order = None
|
|
|
|
print('{}: Order ref: {} / Type {} / Status {}'.format(
|
|
self.data.datetime.date(0),
|
|
order.ref, 'Buy' * order.isbuy() or 'Sell',
|
|
order.getstatusname()))
|
|
|
|
def notify_trade(self, trade):
|
|
print('-' * 50, 'TRADE BEGIN', datetime.datetime.now())
|
|
print(trade)
|
|
print('-' * 50, 'TRADE END')
|
|
|
|
def prenext(self):
|
|
self.next(frompre=True)
|
|
|
|
def next(self, frompre=False):
|
|
# Visualize data0
|
|
txt = list()
|
|
txt.append(str(self.getdatanames()[0]))
|
|
txt.append('%04d' % len(self.data0))
|
|
dtfmt = '%Y-%m-%dT%H:%M:%S.%f'
|
|
txt.append('{:f}'.format(self.data.datetime[0]))
|
|
txt.append('%s' % self.data.datetime.datetime(0).strftime(dtfmt))
|
|
txt.append('{:f}'.format(self.data.open[0]))
|
|
txt.append('{:f}'.format(self.data.high[0]))
|
|
txt.append('{:f}'.format(self.data.low[0]))
|
|
txt.append('{:f}'.format(self.data.close[0]))
|
|
txt.append('{:6d}'.format(int(self.data.volume[0])))
|
|
txt.append('{:d}'.format(int(self.data.openinterest[0])))
|
|
txt.append('{:f}'.format(self.sma[0]))
|
|
print(', '.join(txt))
|
|
|
|
# Visualize data1
|
|
if len(self.datas) > 1 and len(self.data1):
|
|
txt = list()
|
|
txt.append(str(self.getdatanames()[1]))
|
|
txt.append('%04d' % len(self.data1))
|
|
dtfmt = '%Y-%m-%dT%H:%M:%S.%f'
|
|
txt.append('{}'.format(self.data1.datetime[0]))
|
|
txt.append('%s' % self.data1.datetime.datetime(0).strftime(dtfmt))
|
|
txt.append('{}'.format(self.data1.open[0]))
|
|
txt.append('{}'.format(self.data1.high[0]))
|
|
txt.append('{}'.format(self.data1.low[0]))
|
|
txt.append('{}'.format(self.data1.close[0]))
|
|
txt.append('{}'.format(self.data1.volume[0]))
|
|
txt.append('{}'.format(self.data1.openinterest[0]))
|
|
txt.append('{}'.format(float('NaN')))
|
|
print(', '.join(txt))
|
|
|
|
if self.counttostop: # stop after x live lines
|
|
self.counttostop -= 1
|
|
if not self.counttostop:
|
|
self.env.runstop()
|
|
return
|
|
|
|
if not self.p.trade:
|
|
print('No trading. Parameter trade:', self.p.trade)
|
|
return
|
|
|
|
if self.datastatus and not self.position and len(self.orderid) < 1:
|
|
if not self.p.usebracket:
|
|
if not self.p.sell:
|
|
# price = round(self.data0.close[0] * 0.90, 2)
|
|
price = self.data0.close[0] - 5
|
|
self.order = self.buy(size=self.p.stake,
|
|
exectype=self.p.exectype,
|
|
price=price,
|
|
valid=self.p.valid,
|
|
magic=31416)
|
|
else:
|
|
# price = round(self.data0.close[0] * 1.10, 4)
|
|
price = self.data0.close[0] - 0.05
|
|
self.order = self.sell(size=self.p.stake,
|
|
exectype=self.p.exectype,
|
|
price=price,
|
|
valid=self.p.valid)
|
|
|
|
else:
|
|
print('USING BRACKET')
|
|
price = self.data0.close[0] - 0.05
|
|
self.order, _, _ = self.buy_bracket(size=self.p.stake,
|
|
exectype=bt.Order.Market,
|
|
price=price,
|
|
stopprice=price - 0.10,
|
|
limitprice=price + 0.10,
|
|
valid=self.p.valid)
|
|
|
|
self.orderid.append(self.order)
|
|
elif self.position and not self.p.donotcounter:
|
|
if self.order is None:
|
|
if not self.p.sell:
|
|
self.order = self.sell(size=self.p.stake // 2,
|
|
exectype=bt.Order.Market,
|
|
price=self.data0.close[0])
|
|
else:
|
|
self.order = self.buy(size=self.p.stake // 2,
|
|
exectype=bt.Order.Market,
|
|
price=self.data0.close[0])
|
|
|
|
self.orderid.append(self.order)
|
|
|
|
elif self.order is not None and self.p.cancel:
|
|
if self.datastatus > self.p.cancel:
|
|
self.cancel(self.order)
|
|
|
|
if self.datastatus:
|
|
self.datastatus += 1
|
|
|
|
def start(self):
|
|
|
|
header = ['Datetime', 'Open', 'High', 'Low', 'Close', 'Volume',
|
|
'OpenInterest', 'SMA']
|
|
print(', '.join(header))
|
|
|
|
self.done = False
|
|
|
|
|
|
def runstrategy():
|
|
args = parse_args()
|
|
|
|
# Create a cerebro
|
|
cerebro = bt.Cerebro()
|
|
|
|
storekwargs = dict(
|
|
# ram key_id=args.keyid,
|
|
# ram secret_key=args.secretkey,
|
|
# ram paper=not args.live,
|
|
host="192.168.0.71",
|
|
)
|
|
|
|
store = StoreCls(**storekwargs)
|
|
|
|
broker = store.getbroker() # MTraderBroker
|
|
cerebro.setbroker(broker)
|
|
|
|
timeframe = bt.TimeFrame.TFrame(args.timeframe)
|
|
# Manage data1 parameters
|
|
tf1 = args.timeframe1
|
|
tf1 = bt.TimeFrame.TFrame(tf1) if tf1 is not None else timeframe
|
|
cp1 = args.compression1
|
|
cp1 = cp1 if cp1 is not None else args.compression
|
|
|
|
if args.resample or args.replay:
|
|
datatf = datatf1 = bt.TimeFrame.Ticks
|
|
datacomp = datacomp1 = 1
|
|
else:
|
|
datatf = timeframe
|
|
datacomp = args.compression
|
|
datatf1 = tf1
|
|
datacomp1 = cp1
|
|
|
|
fromdate = None
|
|
if args.fromdate:
|
|
dtformat = '%Y-%m-%d' + ('T%H:%M:%S' * ('T' in args.fromdate))
|
|
fromdate = datetime.datetime.strptime(args.fromdate, dtformat)
|
|
|
|
DataFactory = store.getdata # MTraderData
|
|
|
|
datakwargs = dict(
|
|
timeframe=datatf, compression=datacomp,
|
|
qcheck=args.qcheck,
|
|
historical=args.historical,
|
|
fromdate=fromdate,
|
|
bidask=args.bidask,
|
|
useask=args.useask,
|
|
backfill_start=not args.no_backfill_start,
|
|
backfill=not args.no_backfill,
|
|
tz=args.timezone
|
|
)
|
|
|
|
# if args.no_store and not args.broker: # neither store nor broker
|
|
# datakwargs.update(storekwargs) # pass the store args over the data
|
|
|
|
data0 = DataFactory(dataname=args.data0, **datakwargs)
|
|
|
|
data1 = None
|
|
if args.data1 is not None:
|
|
if args.data1 != args.data0:
|
|
datakwargs['timeframe'] = datatf1
|
|
datakwargs['compression'] = datacomp1
|
|
data1 = DataFactory(dataname=args.data1, **datakwargs)
|
|
#ram
|
|
print(datakwargs)
|
|
else:
|
|
data1 = data0
|
|
|
|
rekwargs = dict(
|
|
timeframe=timeframe,
|
|
compression=args.compression,
|
|
bar2edge=not args.no_bar2edge,
|
|
adjbartime=not args.no_adjbartime,
|
|
rightedge=not args.no_rightedge,
|
|
takelate=not args.no_takelate,
|
|
)
|
|
|
|
if args.replay:
|
|
cerebro.replaydata(data0, **rekwargs)
|
|
|
|
if data1 is not None:
|
|
rekwargs['timeframe'] = tf1
|
|
rekwargs['compression'] = cp1
|
|
cerebro.replaydata(data1, **rekwargs)
|
|
|
|
elif args.resample:
|
|
cerebro.resampledata(data0, **rekwargs)
|
|
|
|
if data1 is not None:
|
|
rekwargs['timeframe'] = tf1
|
|
rekwargs['compression'] = cp1
|
|
cerebro.resampledata(data1, **rekwargs)
|
|
|
|
else:
|
|
cerebro.adddata(data0)
|
|
if data1 is not None:
|
|
cerebro.adddata(data1)
|
|
|
|
if args.valid is None:
|
|
valid = None
|
|
else:
|
|
valid = datetime.timedelta(seconds=args.valid)
|
|
# Add the strategy
|
|
cerebro.addstrategy(TestStrategy,
|
|
smaperiod=args.smaperiod,
|
|
trade=args.trade,
|
|
exectype=bt.Order.ExecType(args.exectype),
|
|
stake=args.stake,
|
|
stopafter=args.stopafter,
|
|
valid=valid,
|
|
cancel=args.cancel,
|
|
donotcounter=args.donotcounter,
|
|
sell=args.sell,
|
|
usebracket=args.usebracket)
|
|
|
|
# Live data ... avoid long data accumulation by switching to "exactbars"
|
|
cerebro.run(exactbars=args.exactbars)
|
|
if args.exactbars < 1: # plotting is possible
|
|
if args.plot:
|
|
pkwargs = dict(style='line')
|
|
if args.plot is not True: # evals to True but is not True
|
|
npkwargs = eval('dict(' + args.plot + ')') # args were passed
|
|
pkwargs.update(npkwargs)
|
|
|
|
cerebro.plot(**pkwargs)
|
|
|
|
|
|
def parse_args(pargs=None):
|
|
parser = argparse.ArgumentParser(
|
|
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
|
description='Test Metatrader integration')
|
|
|
|
parser.add_argument('--exactbars', default=1, type=int,
|
|
required=False, action='store',
|
|
help='exactbars level, use 0/-1/-2 to enable plotting')
|
|
|
|
parser.add_argument('--stopafter', default=0, type=int,
|
|
required=False, action='store',
|
|
help='Stop after x lines of LIVE data')
|
|
|
|
parser.add_argument('--debug',
|
|
required=False, action='store_true',
|
|
help='Display all info received from source')
|
|
|
|
parser.add_argument('--keyid', default=None,
|
|
required=False, action='store',
|
|
help='MT5 API key id')
|
|
|
|
parser.add_argument('--secretkey', default=None,
|
|
required=False, action='store',
|
|
help='MT5 API secret key')
|
|
|
|
parser.add_argument('--live', default=None,
|
|
required=False, action='store',
|
|
help='Go to live server rather than paper')
|
|
|
|
parser.add_argument('--qcheck', default=0.5, type=float,
|
|
required=False, action='store',
|
|
help=('Timeout for periodic '
|
|
'notification/resampling/replaying check'))
|
|
|
|
parser.add_argument('--data0', default=None,
|
|
required=True, action='store',
|
|
help='data 0 into the system')
|
|
|
|
parser.add_argument('--data1', default=None,
|
|
required=False, action='store',
|
|
help='data 1 into the system')
|
|
|
|
parser.add_argument('--timezone', default=None,
|
|
required=False, action='store',
|
|
help='timezone to get time output into (pytz names)')
|
|
|
|
parser.add_argument('--bidask', default=None,
|
|
required=False, action='store_true',
|
|
help='Use bidask ... if False use midpoint')
|
|
|
|
parser.add_argument('--useask', default=None,
|
|
required=False, action='store_true',
|
|
help='Use the "ask" of bidask prices/streaming')
|
|
|
|
parser.add_argument('--no-backfill_start',
|
|
required=False, action='store_true',
|
|
help='Disable backfilling at the start')
|
|
|
|
parser.add_argument('--no-backfill',
|
|
required=False, action='store_true',
|
|
help='Disable backfilling after a disconnection')
|
|
|
|
parser.add_argument('--historical',
|
|
required=False, action='store_true',
|
|
help='do only historical download')
|
|
|
|
parser.add_argument('--fromdate',
|
|
required=True, action='store',
|
|
help=('Starting date for historical download '
|
|
'with format: YYYY-MM-DD[THH:MM:SS]'))
|
|
|
|
parser.add_argument('--smaperiod', default=5, type=int,
|
|
required=False, action='store',
|
|
help='Period to apply to the Simple Moving Average')
|
|
|
|
pgroup = parser.add_mutually_exclusive_group(required=False)
|
|
|
|
pgroup.add_argument('--replay',
|
|
required=False, action='store_true',
|
|
help='replay to chosen timeframe')
|
|
|
|
pgroup.add_argument('--resample',
|
|
required=False, action='store_true',
|
|
help='resample to chosen timeframe')
|
|
|
|
parser.add_argument('--timeframe', default='Minutes',
|
|
choices=bt.TimeFrame.Names,
|
|
required=False, action='store',
|
|
help='TimeFrame for Resample/Replay')
|
|
|
|
parser.add_argument('--compression', default=1, type=int,
|
|
required=False, action='store',
|
|
help='Compression for Resample/Replay')
|
|
|
|
parser.add_argument('--timeframe1', default=None,
|
|
choices=bt.TimeFrame.Names[4],
|
|
required=False, action='store',
|
|
help='TimeFrame for Resample/Replay - Data1')
|
|
|
|
parser.add_argument('--compression1', default=None, type=int,
|
|
required=False, action='store',
|
|
help='Compression for Resample/Replay - Data1')
|
|
|
|
parser.add_argument('--no-takelate',
|
|
required=False, action='store_true',
|
|
help=('resample/replay, do not accept late samples'))
|
|
|
|
parser.add_argument('--no-bar2edge',
|
|
required=False, action='store_true',
|
|
help='no bar2edge for resample/replay')
|
|
|
|
parser.add_argument('--no-adjbartime',
|
|
required=False, action='store_true',
|
|
help='no adjbartime for resample/replay')
|
|
|
|
parser.add_argument('--no-rightedge',
|
|
required=False, action='store_true',
|
|
help='no rightedge for resample/replay')
|
|
|
|
parser.add_argument('--trade',
|
|
required=False, action='store_true',
|
|
help='Do Sample Buy/Sell operations')
|
|
|
|
parser.add_argument('--sell',
|
|
required=False, action='store_true',
|
|
help='Start by selling')
|
|
|
|
parser.add_argument('--usebracket',
|
|
required=False, action='store_true',
|
|
help='Test buy_bracket')
|
|
|
|
parser.add_argument('--donotcounter',
|
|
required=False, action='store_true',
|
|
help='Do not counter the 1st operation')
|
|
|
|
parser.add_argument('--exectype', default=bt.Order.ExecTypes[0],
|
|
choices=bt.Order.ExecTypes,
|
|
required=False, action='store',
|
|
help='Execution to Use when opening position')
|
|
|
|
parser.add_argument('--stake', default=0.1, type=float,
|
|
required=False, action='store',
|
|
help='Stake to use in buy operations')
|
|
|
|
parser.add_argument('--valid', default=None, type=float,
|
|
required=False, action='store',
|
|
help='Seconds to keep the order alive (0 means DAY)')
|
|
|
|
parser.add_argument('--cancel', default=0, type=int,
|
|
required=False, action='store',
|
|
help=('Cancel a buy order after n bars in operation,'
|
|
' to be combined with orders like Limit'))
|
|
|
|
# Plot options
|
|
parser.add_argument('--plot', '-p', nargs='?', required=False,
|
|
metavar='kwargs', const=True,
|
|
help=('Plot the read data applying any kwargs passed\n'
|
|
'\n'
|
|
'For example (escape the quotes if needed):\n'
|
|
'\n'
|
|
' --plot style="candle" (to plot candles)\n'))
|
|
|
|
if pargs is not None:
|
|
return parser.parse_args(pargs)
|
|
|
|
return parser.parse_args()
|
|
|
|
|
|
if __name__ == '__main__':
|
|
start_date = datetime.datetime.now() - datetime.timedelta(minutes=500)
|
|
sys.argv = [
|
|
'MTtest2.py',
|
|
'--data0', 'BTCEUR',
|
|
'--timeframe', 'Minutes',
|
|
'--compression', '1',
|
|
'--data1', 'BTCUSD',
|
|
'--timeframe1', 'Minutes',
|
|
'--compression1', '1',
|
|
'--fromdate', start_date.strftime("%Y-%m-%dT%H:%M:%S"),
|
|
'--trade']
|
|
runstrategy()
|