Python 量化策略回测框架


(kikidai) #1

就直接上干货吧(小编实在太忙,奔跑嘛:),整个量化策略的回测框架都是自己写的,这样做的初衷就是尽可能地可以customize策略本身和回测系统。

整个框架由三部分组成:策略(Magi.py),数据(DataHub.py),交易执行(xMan.py)。

下面贴出的策略在这就不细讲了,相信有心的读者一定能读懂。我做过很多回测,效果还不错,大家可以在此基础上不断提高哈。

大家如果有什么问题,尽管私信我。欢迎转载,或者提建议哈,一同进步嘛!

请忽略我如此fancy的命名,哈哈哈。

  1. DataHub.py:数据的下载初步处理

    import pandas_datareader.data as web
    import logging
    import datetime
    import numpy as np

    DATA_SOURCE_YAHOO = ‘yahoo’
    DATA_SOURCE_GOOGLE = ‘google’

    class DataHub:
    def init(self):
    pass

    def _downloadData(self, startDate=datetime.date(2017, 1, 1), endDate=datetime.date.today(), symbols=['AAPL', 'SPY'], dataSource=DATA_SOURCE_YAHOO):
        """
        Downland stock historical data from Yahoo finance, histPanel is a Panel, already in ascending order, e.g:
        Dimensions: 6 (items) x 2 (major_axis) x 2 (minor_axis)
        Items axis(0): Open to Adj Close
        Major_axis axis(1): 2016-10-11 00:00:00 to 2016-10-12 00:00:00
        Minor_axis axis(2): SPY to ^N225
    
        We now use dict of DataFrames to replace panel as Panel is depreciated in Pandas.
        key: symbol
        value: DataFrame with dates as index and "Open" etc as columns
    
        Now we allow different indexes across different symbol DataFrames
        And we will simply remove all 0 or NaN in every DataFrame
        """
        symbolData = dict()
        for symbol in symbols:
            try:
                df = web.DataReader(symbol, dataSource, startDate, endDate)
            except:
                logging.error('DataHub: _downloadData: Cannot download historical data for symbol={}'.format(symbol))
                continue
            symbolData[symbol] = df
    
        # Cleanse data: remove dates where there is NaN or 0
        for symbol, df in symbolData.iteritems():
            df = df.replace(0, np.nan)
            df = df.dropna()
            # Remove duplicated date index
            df = df[~df.index.duplicated(keep='first')]
            symbolData[symbol] = df.sort_index(ascending=True)
    
        logging.info('============================================================')
        logging.info('DataHub: downlaodData: Completed startDate={}, endDate={}'.format(startDate, endDate))
        logging.info('============================================================')
        return symbolData
    
    def downloadDataFromYahoo(self, startDate, endDate, symbols):
        return self._downloadData(startDate, endDate, symbols, DATA_SOURCE_YAHOO)
    
    def downloadDataFromGoogle(self, startDate, endDate, symbols):
        return self._downloadData(startDate, endDate, symbols, DATA_SOURCE_GOOGLE)
    
  2. xMan.py:交易执行过程中order tracking, position tracking, performance evaluation等

    import uuid
    import logging
    import datetime

    ORDER_STATE_NEW = ‘ORDER_STATE_NEW’
    ORDER_STATE_PARTIALLY_FILLED = ‘ORDER_STATE_PARTIALLY_FILLED’
    ORDER_STATE_FULLY_FILLED = ‘ORDER_STATE_FULLY_FILLED’
    ORDER_STATE_CANCELLED = ‘ORDER_STATE_CANCELLED’
    ORDER_TYPE_MARKET = ‘ORDER_TYPE_MARKET’
    ORDER_TYPE_LIMIT = ‘ORDER_TYPE_LIMIT’
    ORDER_TYPE_STOP = ‘ORDER_TYPE_STOP’
    ORDER_DIRECTION_BUY = ‘ORDER_DIRECTION_BUY’
    ORDER_DIRECTION_SELL = ‘ORDER_DIRECTION_SELL’

    MIN_COMMISSION_PER_ORDER = 1
    MAX_COMMISSION_PER_ORDER_PER_TRADE_VALUE = 0.005
    COMMISSION_PER_SHARE = 0.005

    class Order:
    def init(self, symbol, direction, type, price, quantity, openDtIdx):
    self.orderId = uuid.uuid4()
    self.symbol = symbol
    self.direction = direction
    self.type = type
    # this is the price submitted when placing order
    self.price = price
    # this is the average price filling the order
    self.fillPrice = 0
    # this quantity should be non-negative
    self.quantityOutstanding = quantity
    self.quantityFilled = 0
    self.state = ORDER_STATE_NEW
    self.commission = 0
    self.linkId = None
    # this is when the order is created
    self.openDtIdx = openDtIdx
    # this is when the order is fully filled or cancelled
    self.closeDtIdx = None

    def __str__(self):
        return 'Order<orderId={}, symbol={}, direction={}, type={}, price={}, fillPrice={}, quantityOutstanding={}, quantityFilled={}, state={}, commission={}, linkId={}, openDtIdx={}, closeDtIdx={}>'.format(self.orderId, self.symbol, self.direction, self.type, self.price, self.fillPrice, self.quantityOutstanding, self.quantityFilled, self.state, self.commission, self.linkId, self.openDtIdx, self.closeDtIdx)
    
    def calculateCommission(self):
        """Calculate commission for this order, commission is only incurred on fully filled"""
        minCommission = MIN_COMMISSION_PER_ORDER
        maxCommission = MAX_COMMISSION_PER_ORDER_PER_TRADE_VALUE * (self.fillPrice * self.quantityFilled)
        commission = COMMISSION_PER_SHARE * self.quantityFilled
        self.commission = max(min(commission, maxCommission), minCommission)
    
    def fill(self, fillPrice, quantity, datetime):
        logging.info('Order: fill: BEFORE: order={} CHANGE: fillPrice={}, quantity={}, datetime={}'.format(self, fillPrice, quantity, datetime))
        if quantity > self.quantityOutstanding or quantity <= 0:
            logging.error('Order: fill: orderId={} Invalid quantity={}!'.format(self.orderId, quantity))
            raise Exception()
        self.fillPrice = (self.fillPrice * self.quantityFilled + fillPrice * quantity) / (self.quantityFilled + quantity)
        self.quantityFilled += quantity
        self.quantityOutstanding -= quantity
        if self.quantityOutstanding > 0:
            self.state = ORDER_STATE_PARTIALLY_FILLED
        elif self.quantityOutstanding == 0:
            self.state = ORDER_STATE_FULLY_FILLED
            self.closeDtIdx = datetime
            self.calculateCommission()
        logging.info('Order: fill: AFTER: order={} CHANGE: fillPrice={}, quantity={}, datetime={}'.format(self, fillPrice, quantity, datetime))
    
    def cancel(self, datetime):
        logging.info('Order: cancel: BEFORE: order={} CHANGE: datetime={}'.format(self, datetime))
        self.state = ORDER_STATE_CANCELLED
        self.closeDtIdx = datetime
        logging.info('Order: cancel: AFTER: order={} CHANGE: datetime={}'.format(self, datetime))
    

    class Position:
    def init(self, symbol):
    self.symbol = symbol
    # Can be short position, where quantity is negative
    self.quantity = 0
    self.cost = 0
    self.realizedPNL = 0

    def __str__(self):
        return 'Position<symbol={}, quantity={}, cost={}, realizedPNL={}>'.format(self.symbol, self.quantity, self.cost, self.realizedPNL)
    
    def change(self, price, quantity, commission):
        """Position change should ONLY triggered by order execution"""
        logging.info('Position: BEFORE: position={} CHANGE: price={}, quantity={}, commission={}'.format(self, price, quantity, commission))
        if quantity == 0:
            logging.error('Position: change: symbol={} Invalid quantity is 0'.format(self.symbol))
            raise Exception()
        if self.quantity * quantity >= 0:
            # Increasing position, either short or long, commission is included in position cost
            self.quantity += quantity
            self.cost += (price * quantity + commission)
        else:
            # Reducing position, either short or long, commission is included in realizedPNL
            self.realizedPNL += ((self.cost/self.quantity - price)*quantity - commission)
            self.cost += self.cost/self.quantity*quantity
            self.quantity += quantity
        logging.info('Position: AFTER: position={} CHANGE: price={}, quantity={}, commission={}'.format(self, price, quantity, commission))
    

    class MarketTick:
    “”"
    MarketTick is symbol specific, which simulates the real-time market data tick.
    In reality, different symbols may tick at different time.
    “”"
    def init(self, symbol, open, close, high, low, volume, dtIdx):
    self.symbol = symbol
    self.open = open
    self.close = close
    self.high = high
    self.low = low
    self.volume = volume
    self.dtIdx = dtIdx

    def __str__(self):
        return 'MarketTick<symbol={}, open={}, close={}, high={}, low={}, volume={}, dtIdx={}>'.format(self.symbol, self.open, self.close, self.high, self.low, self.volume, self.dtIdx)
    

    class Performance:
    “”“Performance metrics for each symbol”""
    def init(self, symbol):
    self.symbol = symbol
    self.outstandingMarketOrders = 0
    self.outstandingStopOrders = 0
    self.outstandingLimitOrders = 0
    self.filledMarketOrders = 0
    self.filledStopOrders = 0
    self.filledLimitOrders = 0
    self.cancelledMarketOrders = 0
    self.cancelledStopOrders = 0
    self.cancelledLimitOrders = 0
    self.success = 0
    self.failure = 0
    self.maxCapitalRequired = 0
    self.realizedPNL = 0
    self.positionQuantity = 0
    self.positionCost = 0
    self.totalTradeLife = datetime.timedelta()

    def __str__(self):
        return 'Performance<symbol={}, outstandingMarketOrders={}, outstandingStopOrders={}, outstandingLimitOrders={}, filledMarketOrders={}, filledStopOrders={}, filledLimitOrders={}, cancelledMarketOrders={}, cancelledStopOrders={}, cancelledLimitOrders={}, success={}, failure={}, successRate={:.2f}%, maxCapitalRequired={}, realizedPNL={}, positionQuantity={}, positionCost={}, totalTradeLife={}, averageTradeLife={}>'.format(self.symbol, self.outstandingMarketOrders, self.outstandingStopOrders, self.outstandingLimitOrders, self.filledMarketOrders, self.filledStopOrders, self.filledLimitOrders, self.cancelledMarketOrders, self.cancelledStopOrders, self.cancelledLimitOrders, self.success, self.failure, float(self.success*100)/(self.success+self.failure) if self.success+self.failure > 0 else float('nan'), self.maxCapitalRequired, self.realizedPNL, self.positionQuantity, self.positionCost, self.totalTradeLife, self.totalTradeLife/(self.success+self.failure) if self.success+self.failure > 0 else 'No Trades')
    
    def updatePerformance(self, outstandingMarketOrders, outstandingStopOrders, outstandingLimitOrders, filledMarketOrders, filledStopOrders, filledLimitOrders, cancelledMarketOrders, cancelledStopOrders, cancelledLimitOrders, success, failure, maxCapitalRequired, realizedPNL, positionQuantity, positionCost, totalTradeLife):
        self.outstandingMarketOrders = outstandingMarketOrders
        self.outstandingStopOrders = outstandingStopOrders
        self.outstandingLimitOrders = outstandingLimitOrders
        self.filledMarketOrders = filledMarketOrders
        self.filledStopOrders = filledStopOrders
        self.filledLimitOrders = filledLimitOrders
        self.cancelledMarketOrders = cancelledMarketOrders
        self.cancelledStopOrders = cancelledStopOrders
        self.cancelledLimitOrders = cancelledLimitOrders
        self.success = success
        self.failure = failure
        self.maxCapitalRequired = maxCapitalRequired
        self.realizedPNL = realizedPNL
        self.positionQuantity = positionQuantity
        self.positionCost = positionCost
        self.totalTradeLife = totalTradeLife
    

    class xMan:
    def init(self, initialCapital):
    self.orders = []
    self.positions = []
    self.performances = []
    self.portfolioRealizedPNL = 0
    self.portfolioCashBalance = initialCapital
    self.initialCapital = initialCapital
    self.portfolioPositionCost = 0
    self.portfolioMaxCapitalRequired = 0
    self.portfolioSuccess = 0
    self.portfolioFailure = 0

    def placeOrder(self, order):
        self.orders.append(order)
    
    def getOrderByOrderId(self, orderId):
        """Return a single order or None given the unique orderId"""
        for order in self.orders:
            if order.orderId == orderId:
                return order
        logging.debug('xMan: getOrderByOrderId: No order found for orderId={}'.format(orderId))
    
    def getOrdersByLinkId(self, linkId):
        """Return a list of orders given the linkId"""
        orders = []
        for order in self.orders:
            if order.linkId and order.linkId == linkId:
                orders.append(order)
        return orders
    
    def getOrdersBySymbol(self, symbol):
        """Return a list of orders given the symbol"""
        orders = []
        for order in self.orders:
            if order.symbol == symbol:
                orders.append(order)
        return orders
    
    def getPositionBySymbol(self, symbol):
        for position in self.positions:
            if position.symbol == symbol:
                return position
        logging.debug('xMan: getPositionBySymbol: No position found for symbol={}'.format(symbol))
    
    def getPerformanceBySymbol(self, symbol):
        for performance in self.performances:
            if performance.symbol == symbol:
                return performance
        logging.debug('xMan: getPerformanceBySymbol: No position found for symbol={}'.format(symbol))
    
    def executeMarketOrder(self, order, marketTick):
        """Execute market order, update position"""
        logging.debug('xMan: executeMarketOrder: Check order={}, marketTick={}'.format(order, marketTick))
        # We always fully fill market orders
        quantityChanged = order.quantityOutstanding if order.direction == ORDER_DIRECTION_BUY else -order.quantityOutstanding
        order.fill(marketTick.open, order.quantityOutstanding, marketTick.dtIdx)
        if order.state == ORDER_STATE_FULLY_FILLED:
            self.cancelLinkedOrders(order, marketTick.dtIdx)
    
        position = self.getPositionBySymbol(order.symbol)
        if not position:
            position = Position(order.symbol)
            self.positions.append(position)
        position.change(marketTick.open, quantityChanged, order.commission)
    
    def executeLimitOrder(self, order, marketTick):
        """Execute limit order, update position"""
        logging.debug('xMan: executeLimitOrder: Check order={}, marketTick={}'.format(order, marketTick))
        if (order.direction == ORDER_DIRECTION_BUY and order.price >= marketTick.low) or (order.direction == ORDER_DIRECTION_SELL and order.price <= marketTick.high):
            quantityChanged = order.quantityOutstanding if order.direction == ORDER_DIRECTION_BUY else -order.quantityOutstanding
            order.fill(order.price, order.quantityOutstanding, marketTick.dtIdx)
            if order.state == ORDER_STATE_FULLY_FILLED:
                self.cancelLinkedOrders(order, marketTick.dtIdx)
    
            position = self.getPositionBySymbol(order.symbol)
            if not position:
                position = Position(order.symbol)
                self.positions.append(position)
            position.change(order.price, quantityChanged, order.commission)
    
    def executeStopOrder(self, order, marketTick):
        """Execute stop order, update position"""
        logging.debug('xMan: executeStopOrder: Check order={}, marketTick={}'.format(order, marketTick))
        if (order.direction == ORDER_DIRECTION_BUY and order.price <= marketTick.high) or (order.direction == ORDER_DIRECTION_SELL and order.price >= marketTick.low):
            quantityChanged = order.quantityOutstanding if order.direction == ORDER_DIRECTION_BUY else -order.quantityOutstanding
            order.fill(order.price, order.quantityOutstanding, marketTick.dtIdx)
            if order.state == ORDER_STATE_FULLY_FILLED:
                self.cancelLinkedOrders(order, marketTick.dtIdx)
    
            position = self.getPositionBySymbol(order.symbol)
            if not position:
                position = Position(order.symbol)
                self.positions.append(position)
            position.change(order.price, quantityChanged, order.commission)
    
    def executeOrdersOnMarketTick(self, marketTick):
        for order in self.getOrdersBySymbol(marketTick.symbol):
            if order.state in [ORDER_STATE_FULLY_FILLED, ORDER_STATE_CANCELLED]:
                continue
            if order.type == ORDER_TYPE_MARKET:
                self.executeMarketOrder(order, marketTick)
            #TODO: We execute stop order ahead of limit order, limit order can possibly be cancelled before filled
            elif order.type == ORDER_TYPE_STOP:
                self.executeStopOrder(order, marketTick)
            elif order.type == ORDER_TYPE_LIMIT:
                self.executeLimitOrder(order, marketTick)
            else:
                logging.error('xMan: executeOrdersOnMarketTick: Unsupported order type {}'.format(order))
    
    def cancelLinkedOrders(self, order, datetime):
        """If one order get fully filled, other linked orders will be cancelled"""
        linkedOrders = self.getOrdersByLinkId(order.linkId)
        for linkedOrder in linkedOrders:
            if linkedOrder.orderId != order.orderId:
                linkedOrder.cancel(datetime)
    
    def linkOrders(self, orders):
        """If one order get fully filled, other linked orders will be cancelled"""
        linkId = uuid.uuid4()
        for order in orders:
            order.linkId = linkId
        logging.debug('xMan: linkOrders: Linked orderIds={}, linkId={}'.format([order.orderId for order in orders], linkId))
    
    def getAllSymbols(self):
        """Get all symbols ever executed"""
        orderSymbols = [order.symbol for order in self.orders]
        positionSymbols = [position.symbol for position in self.positions]
        return list(set(orderSymbols + positionSymbols))
    
    def evaluatePerformance(self):
        self.portfolioRealizedPNL = 0
        self.portfolioCashBalance = self.initialCapital
        self.portfolioPositionCost = 0
        self.portfolioSuccess = 0
        self.portfolioFailure = 0
        self.portfolioTotalTradeLife = datetime.timedelta()
        for symbol in self.getAllSymbols():
            outstandingMarketOrders, outstandingStopOrders, outstandingLimitOrders, filledMarketOrders, filledStopOrders, filledLimitOrders, cancelledMarketOrders, cancelledStopOrders, cancelledLimitOrders = 0, 0, 0, 0, 0, 0, 0, 0, 0
            totalTradeLife = datetime.timedelta()
            for order in self.getOrdersBySymbol(symbol):
                if order.state in [ORDER_STATE_PARTIALLY_FILLED, ORDER_STATE_NEW]:
                    if order.type == ORDER_TYPE_MARKET:
                        outstandingMarketOrders += 1
                    elif order.type == ORDER_TYPE_STOP:
                        outstandingStopOrders += 1
                    elif order.type == ORDER_TYPE_LIMIT:
                        outstandingLimitOrders += 1
                elif order.state == ORDER_STATE_FULLY_FILLED:
                    if order.type == ORDER_TYPE_MARKET:
                        filledMarketOrders += 1
                    elif order.type == ORDER_TYPE_STOP:
                        filledStopOrders += 1
                        totalTradeLife += (order.closeDtIdx.to_pydatetime() - order.openDtIdx.to_pydatetime())
                    elif order.type == ORDER_TYPE_LIMIT:
                        filledLimitOrders += 1
                        totalTradeLife += (order.closeDtIdx.to_pydatetime() - order.openDtIdx.to_pydatetime())
                elif order.state == ORDER_STATE_CANCELLED:
                    if order.type == ORDER_TYPE_MARKET:
                        cancelledMarketOrders += 1
                    elif order.type == ORDER_TYPE_STOP:
                        cancelledStopOrders += 1
                    elif order.type == ORDER_TYPE_LIMIT:
                        cancelledLimitOrders += 1
            position = self.getPositionBySymbol(symbol)
            if not position:
                position = Position(symbol)
                self.positions.append(position)
            performance = self.getPerformanceBySymbol(symbol)
            if not performance:
                performance = Performance(symbol)
                self.performances.append(performance)
            success = filledLimitOrders
            failure = filledStopOrders
            maxCapitalRequired = max(performance.maxCapitalRequired, position.cost)
            performance.updatePerformance(outstandingMarketOrders, outstandingStopOrders, outstandingLimitOrders, filledMarketOrders, filledStopOrders, filledLimitOrders, cancelledMarketOrders, cancelledStopOrders, cancelledLimitOrders, success, failure, maxCapitalRequired, position.realizedPNL, position.quantity, position.cost, totalTradeLife)
            logging.info('xMan: evaluatePerformance: performance={}'.format(performance))
            self.portfolioSuccess += success
            self.portfolioFailure += failure
            self.portfolioTotalTradeLife += totalTradeLife
            self.portfolioRealizedPNL += position.realizedPNL
            self.portfolioCashBalance += (position.realizedPNL - position.cost)
            self.portfolioPositionCost += position.cost
        self.portfolioMaxCapitalRequired = max(self.portfolioMaxCapitalRequired, self.portfolioPositionCost)
    
        logging.info('xMan: evaluatePerformance: Portfolio portfolioRealizedPNL={}, portfolioCashBalance={}, portfolioPositionCost={}, portfolioMaxCapitalRequired={}, portfolioSuccess={}, portfolioFailure={}, portfolioSuccessRate={:.2f}%, portfolioAverageTradeLife={}'.format(self.portfolioRealizedPNL, self.portfolioCashBalance, self.portfolioPositionCost, self.portfolioMaxCapitalRequired, self.portfolioSuccess, self.portfolioFailure, (float(self.portfolioSuccess)*100)/(self.portfolioSuccess+self.portfolioFailure) if self.portfolioSuccess+self.portfolioFailure else float('nan'), self.portfolioTotalTradeLife/(self.portfolioSuccess+self.portfolioFailure) if self.portfolioSuccess+self.portfolioFailure > 0 else 'No Trades'))
    
    def describeTradesExecutedByDatetime(self):
        result = dict()
        for order in self.orders:
            if order.state == ORDER_STATE_FULLY_FILLED:
                daycounts = result.get(order.closeDtIdx, dict())
                count = daycounts.get(order.type, 0)
                daycounts[order.type] = count + 1
                result[order.closeDtIdx] = daycounts
        logging.info('============================================================')
        logging.info('Trades Execution Summary for all dates')
        logging.info('------------------------------------------------------------')
        keys = result.keys()
        keys.sort()
        for dt in keys:
            logging.info('xMan: describeTradesExecutedByDatetime: {}: {}'.format(dt, result.get(dt, 'ERROR')))
        logging.info('============================================================')
    
  3. Magi.py:最核心的策略本身了。

    from DataHub import DataHub
    from xMan import Order, MarketTick, Position, xMan, ORDER_STATE_NEW, ORDER_STATE_PARTIALLY_FILLED, ORDER_STATE_FULLY_FILLED, ORDER_STATE_CANCELLED, ORDER_TYPE_MARKET, ORDER_TYPE_LIMIT, ORDER_TYPE_STOP, ORDER_DIRECTION_BUY, ORDER_DIRECTION_SELL
    import logging
    import datetime
    import math
    import pandas

    STOCKS_SELECTED = [‘NFLX’, ‘YUM’, ‘CCE’, ‘WY’, ‘VRTX’, ‘MYL’, ‘GIS’, ‘D’, ‘DHR’, ‘URI’, ‘HRS’, ‘AMGN’, ‘GD’, ‘T’, ‘MAS’, ‘MAR’, ‘IPG’, ‘OI’, ‘SNA’, ‘LEG’, ‘NTRS’, ‘DG’, ‘JNJ’, ‘SYY’, ‘TSN’, ‘BEN’, ‘CMI’, ‘CME’, ‘ESRX’, ‘NVDA’, ‘FDX’, ‘CMS’, ‘DTV’, ‘AVGO’, ‘ETN’, ‘RTN’, ‘AMG’, ‘PG’, ‘GRMN’, ‘BLX’, ‘SCHW’, ‘ED’, ‘FFIV’, ‘XRX’, ‘APC’, ‘UPS’, ‘NFX’, ‘MPC’, ‘MO’, ‘MDLZ’, ‘MS’, ‘IVZ’, ‘DVA’, ‘ACN’, ‘COST’, ‘DOV’, ‘TMO’, ‘NI’, ‘TEL’, ‘FCX’, ‘ECL’, ‘ZBH’, ‘BSK’, ‘NUE’, ‘HP’, ‘LNC’, ‘EXPD’, ‘ETFC’, ‘ADS’, ‘ADP’, ‘SRE’, ‘CB’, ‘CA’, ‘CF’, ‘R’, ‘CLX’, ‘UNH’, ‘ADM’, ‘OMC’, ‘NTAP’, ‘XEC’, ‘MMV’, ‘RLC’, ‘EBAY’, ‘MET’, ‘JNPR’, ‘SE’, ‘KLAC’, ‘BF-B’, ‘GNW’, ‘BXP’, ‘AIV’, ‘CAT’, ‘BAC’, ‘CAG’, ‘TXN’, ‘PPG’, ‘ISRG’, ‘LH’, ‘ORCL’, ‘PGR’, ‘HIG’, ‘ZION’, ‘HON’, ‘IP’, ‘IR’, ‘STI’, ‘MLM’, ‘STT’, ‘ABBV’, ‘ALXN’, ‘JCI’, ‘BDX’, ‘PRU’, ‘CSCO’, ‘PNC’]
    SYMBOLS = STOCKS_SELECTED

    START_DATE = datetime.date(2017,1,1)
    END_DATE = datetime.date(2018,1,1)
    SD_PERIOD = 22

    This is for trading signal logic

    LOOK_BACK_PERIOD = 22

    This allows to trade on next day’s open

    LOOK_FORWARD_PERIOD = 0
    MA_SHORT_PERIOD = 5
    MA_LONG_PERIOD = SD_PERIOD
    TRIGGER_DISTANCE = 3
    STOP_ORDER_DISTANCE = 2
    LIMIT_ORDER_DISTANCE = 2
    ORDER_LIMIT = 1000
    CAPITAL = 10000

    class Magi:
    def init(self):
    self.xMan = xMan(CAPITAL)
    self.dataHub = DataHub()
    self.symbolData = None
    # Strategy config
    self.symbols = SYMBOLS
    self.startDate = START_DATE
    self.endDate = END_DATE
    self.sdPeriod = SD_PERIOD
    self.lookBackPeriod = LOOK_BACK_PERIOD
    self.lookForwardPeriod = LOOK_FORWARD_PERIOD
    self.MAShortPeriod = MA_SHORT_PERIOD
    self.MALongPeriod = MA_LONG_PERIOD
    self.triggerDistance = TRIGGER_DISTANCE
    self.stopOrderDistance = STOP_ORDER_DISTANCE
    self.limitOrderDistance = LIMIT_ORDER_DISTANCE
    self.orderLimit = ORDER_LIMIT
    self.capital = CAPITAL

        self.logConfig()
    
    def __str__(self):
        return 'He is my shield!'
    
    def logConfig(self):
        logging.info('============================================================')
        logging.info('Magi strategy config:')
        logging.info('SYMBOLS; {}'.format(self.symbols))
        logging.info('START_DATE: {}'.format(self.startDate))
        logging.info('END_DATE: {}'.format(self.endDate))
        logging.info('SD_PERIOD: {}'.format(self.sdPeriod))
        logging.info('LOOK_BACK_PERIOD: {}'.format(self.lookBackPeriod))
        logging.info('LOOK_FORWARD_PERIOD: {}'.format(self.lookForwardPeriod))
        logging.info('MA_SHORT_PERIOD: {}'.format(self.MAShortPeriod))
        logging.info('MA_LONG_PERIOD: {}'.format(self.MALongPeriod))
        logging.info('TRIGGER_DISTANCE: {}'.format(self.triggerDistance))
        logging.info('STOP_ORDER_DISTANCE: {}'.format(self.stopOrderDistance))
        logging.info('LIMIT_ORDER_DISTANCE: {}'.format(self.limitOrderDistance))
        logging.info('ORDER_LIMIT: {}'.format(self.orderLimit))
        logging.info('CAPITAL: {}'.format(self.capital))
        logging.info('============================================================')
    
    def setUpConfig(self, symbols=SYMBOLS, startDate=START_DATE, endDate=END_DATE, sdPeriod=SD_PERIOD, lookBackPeriod=LOOK_BACK_PERIOD, lookForwardPeriod=LOOK_FORWARD_PERIOD, MAShortPeriod=MA_SHORT_PERIOD, MALongPeriod=MA_LONG_PERIOD, triggerDistance=TRIGGER_DISTANCE, stopOrderDistance=STOP_ORDER_DISTANCE, limitOrderDistance=LIMIT_ORDER_DISTANCE, orderLimit=ORDER_LIMIT, capital=CAPITAL):
        self.symbols = symbols
        self.startDate = startDate
        self.endDate = endDate
        self.sdPeriod = sdPeriod
        self.lookBackPeriod = lookBackPeriod
        self.lookForwardPeriod = lookForwardPeriod
        self.MAShortPeriod = MAShortPeriod
        self.MALongPeriod = MALongPeriod
        self.triggerDistance = triggerDistance
        self.stopOrderDistance = stopOrderDistance
        self.limitOrderDistance = limitOrderDistance
        self.orderLimit = orderLimit
        self.capital = capital
        self.logConfig()
    
    def prepareSymbolData(self):
        self.symbolData = self.dataHub.downloadDataFromYahoo(self.startDate, self.endDate, self.symbols)
    
    def getStartIndex(self):
        return max(self.sdPeriod, self.lookBackPeriod, self.MAShortPeriod, self.MALongPeriod)
    
    def getEndIndex(self):
        return -self.lookForwardPeriod
    
    def getOrderSize(self, price):
        """Esitmate the order size based on the current price and order limit"""
        #TODO: We need to improve the logic
        return math.floor(self.orderLimit / price)
    
    def runStrategyOnMarketTick(self, marketTick):
        """
        Run strategy for the marketTick given for a specific symbol.
        The strategy probably also depends on past marketTicks, which need to be looked up in self.symbolData
        Place orders based on strategy signals
        """
        # We do not execute strategy if we already have an open position.
        #position = self.xMan.getPositionBySymbol(marketTick.symbol)
        #if position and position.quantity != 0:
        #    return
    
        ts = self.symbolData[marketTick.symbol]['Close']
        sd = ts[:marketTick.dtIdx][-self.sdPeriod:].std()
        highest = ts[:marketTick.dtIdx][-self.lookBackPeriod:].max()
        lowest = ts[:marketTick.dtIdx][-self.lookBackPeriod:].min()
        MAShort = ts[:marketTick.dtIdx][-self.MAShortPeriod:].mean()
        MALong = ts[:marketTick.dtIdx][-self.MALongPeriod:].mean()
        currPrice = marketTick.close
    
        if currPrice < highest - sd * self.triggerDistance and currPrice >= MAShort:
            quantity = self.getOrderSize(currPrice)
            #TODO: Without knowledge of the next marketTick, we place orders based on current marketTick
            if quantity > 0:
                marketOrder = Order(marketTick.symbol, ORDER_DIRECTION_BUY, ORDER_TYPE_MARKET, float('nan'), quantity, marketTick.dtIdx)
                self.xMan.placeOrder(marketOrder)
                logging.info('Magi: runStrategyOnMarketTick: TRIGGER BUY: Placed marketOrder={}'.format(marketOrder))
                stopOrder = Order(marketTick.symbol, ORDER_DIRECTION_SELL, ORDER_TYPE_STOP, currPrice-sd*self.stopOrderDistance, quantity, marketTick.dtIdx)
                limitOrder = Order(marketTick.symbol, ORDER_DIRECTION_SELL, ORDER_TYPE_LIMIT, currPrice + sd * self.limitOrderDistance, quantity, marketTick.dtIdx)
                self.xMan.linkOrders([stopOrder, limitOrder])
                self.xMan.placeOrder(stopOrder)
                logging.info('Magi: runStrategyOnMarketTick: Placed stopOrder={}'.format(stopOrder))
                self.xMan.placeOrder(limitOrder)
                logging.info('Magi: runStrategyOnMarketTick: Placed limitOrder={}'.format(limitOrder))
            else:
                logging.info('Magi: runStrategyOnMarketTick: TRIGGER BUY, but cannot trade due to quantity=0, marketTick={}'.format(marketTick))
    
    def run(self):
        self.prepareSymbolData()
    
        # Determine trading range for running strategy. Exclude periods for strategy setup, endIdx Not inclusive
        dtIndexes = pandas.date_range(self.startDate, self.endDate, freq='B')
        startIdx = self.getStartIndex()
        endIdx = self.getEndIndex()
        logging.debug('Magi: run: startIdx={}, endIdx={}'.format(startIdx, endIdx))
    
        for dtIdx in dtIndexes:
            logging.info('============================================================')
            logging.info(dtIdx)
            logging.info('------------------------------------------------------------')
            logging.info('Execute existing orders and run strategy for today')
            logging.info('------------------------------------------------------------')
            for symbol in self.symbolData.keys():
                if dtIdx not in (self.symbolData[symbol].index[startIdx:] if endIdx == 0 else self.symbolData[symbol].index[startIdx: endIdx]):
                    logging.debug('Magi: run: dtIdx={}: Cannot trade, outside strategy running period'.format(dtIdx))
                    continue
    
                # Construct marketTick for this tradingDate
                open = self.symbolData[symbol].loc[dtIdx, 'Open']
                close = self.symbolData[symbol].loc[dtIdx, 'Close']
                high = self.symbolData[symbol].loc[dtIdx, 'High']
                low = self.symbolData[symbol].loc[dtIdx, 'Low']
                volume = self.symbolData[symbol].loc[dtIdx, 'Volume']
                marketTick = MarketTick(symbol, open, close, high, low, volume, dtIdx)
    
                # Execute existing orders from previous tradingPeriod. In reality, this happens during current tradingPeriod.
                self.xMan.executeOrdersOnMarketTick(marketTick)
    
                # Run strategy on current tradingPeriod. In reality, this happens immediately after current tradingPeriod.
                self.runStrategyOnMarketTick(marketTick)
    
            logging.info('------------------------------------------------------------')
            logging.info('Performance Summary for today')
            logging.info('------------------------------------------------------------')
            self.xMan.evaluatePerformance()
            logging.info('============================================================')
    
        self.xMan.describeTradesExecutedByDatetime()
    

    def main():
    “”“Main function”""
    #logging.basicConfig(format=’%(asctime)s %(levelname)s: %(message)s’, level=logging.INFO)
    logging.basicConfig(filename=‘Magi_{0}.log’.format(datetime.datetime.now().strftime(’%Y-%m-%d_%H-%M-%S’)), format=’%(levelname)s: %(message)s’, level=logging.INFO)
    magi = Magi()
    magi.run()

    if name == ‘main’:
    “”“Entry point”""
    main()