Exemple #1
0
 def add_strategy(self, strategy):
     """
     Add the strategy and set the settings
     """
     self._strategy = strategy
     # create an output instance for the strategy report
     root = os.path.join(os.getcwd(), '..', 'report')
     fname = self._strategy.name + ".report"
     self.o = Output(root, fname)
     # check whether repot file has been created
     if self.o.ReportFileExist == False:
         print(
             "A 'report' folder should be provided in the parent directory of current directory."
         )
Exemple #2
0
 def add_strategy(self, strategy):
     """
     Add the strategy and set the settings
     """
     self._strategy = strategy
     # create an output instance for the strategy report
     root = os.path.join(os.getcwd(), "..", "report")
     fname = self._strategy.name + ".report"
     self.o = Output(root, fname)
     # check whether repot file has been created
     if self.o.ReportFileExist == False:
         print("A 'report' folder should be provided in the parent directory of current directory.")
Exemple #3
0
class ExecuteUnit(object):
    """
    Execute unit of a strategy.
    One execute unit can only load one strategy, and the period \
    candlestick data should all belong to one specific stock. 
    """
    def __init__(self, pstocks, dt_start, dt_end, n_ahead, capital,
                 StampTaxRate, CommissionChargeRate):
        self._strategy = None
        # actual timescope of the back test, depending on the user setting and the available data
        self.timescope = None
        # number of extra daily data for computation (ahead of start datatime)
        # set in the main program
        self.n_ahead = n_ahead
        # an output class instance for the report file
        self.o = None
        # check whether datetime is proper
        assert (dt_start < dt_end)
        # is_Day_cst: to check whether there is at least one daily candlestick data in pstocks
        is_Day_cst_exist = False
        # load candlestick(cst) data
        # .stocks = {code(str): PStock,}
        self.stocks = {}
        for pstock in pstocks:
            # get info from filename
            # .ID(class StockID), .period(str)
            ID, period = parse_cst_filename(pstock)
            if period == '1Day':
                is_Day_cst_exist = True
            if ID.code not in self.stocks:
                self.stocks[ID.code] = PStock(pstock, dt_start, dt_end,
                                              n_ahead)
            else:
                self.stocks[ID.code].load_cst(pstock, dt_start, dt_end,
                                              n_ahead)
        # remind user
        assert is_Day_cst_exist, "At least one item in pstocks should be daily candlestick data"
        # for convenience, store sz cst information as class PStock
        self.sz = PStock('000001.sz-1Day', dt_start, dt_end, n_ahead)
        # for convenience, store dayily close price of sz
        self.sz_daily_close = self.sz.cst['1Day']['close'].values[n_ahead:]
        # get sz daily time axis
        self.szTimeAxis = self.sz.cst['1Day'].index[
            n_ahead:]  # pd.DataFrame.index
        # find missing daily candlestick data and fill them up in .stocks[code].missing_cst['1Day']
        # as pd.DataFrame
        self.fill_missing_cst()
        # read candlestick data timescope from sz
        self.timescope = str(self.szTimeAxis[0]), str(self.szTimeAxis[-1])
        # initial capital to invest, a constant once being set
        self._capital = capital
        # cash in the account, changing during the back test
        self._cash = capital
        # stock shares in the account, changing during the back test
        self._shares = {}  # ._shares: {code(str): quantity(int)}
        # records. add one record after each transaction(buy or sell)
        # _records = [(datetime(str), cash, shares),]
        self._records = []
        self._records.append(
            (self.timescope[0], self._cash, self._shares.copy()))
        # set stamp tax rate and commission charge rate
        self.StampTaxRate = StampTaxRate
        self.CommissionChargeRate = CommissionChargeRate
        # to store total commission charge in the back test
        self._total_commission_charge = 0
        # to store total stamp tax in the back test
        self._total_stamp_tax = 0
        # trading_stocks: To store the stocks that are traded. Note that although have been loaded,
        #                 some stocks in self.stocks may not been traded in user's strategy.
        self.trading_stocks = set()

    def fill_missing_cst(self):
        """ 
        Find missing daily candlestick data and fill them up 
        in .stocks[code].missing_cst['1Day'] as pd.DataFrame 
        """
        for code in self.stocks.keys():
            # get pd.DataFrame.columns of cst data
            columns = self.stocks[code].cst['1Day'].columns
            # tmp_cst, tmp_tickers: to store missing daily candlestick data
            tmp_cst = {column: [] for column in columns}
            tmp_tickers = []
            # clear ivar: last_normal_cst at the beginning of each loop
            last_normal_cst = None
            # start to search missing cst data
            atbegin_flag = True
            for ticker in self.szTimeAxis:
                if atbegin_flag:  # if missing cst data at the beginning, fill up with zeros
                    try:
                        last_normal_cst = self.stocks[code].cst['1Day'].ix[
                            ticker]
                    except KeyError:
                        tmp_tickers.append(ticker)
                        for column in columns:
                            tmp_cst[column].append(0.0)
                    else:
                        atbegin_flag = False
                else:
                    try:
                        last_normal_cst = self.stocks[code].cst['1Day'].ix[
                            ticker]
                    except KeyError:
                        tmp_tickers.append(ticker)
                        for column in columns:
                            tmp_cst[column].append(last_normal_cst[column])
                    else:
                        pass
            # store missing cst as pd.DataFrame
            self.stocks[code].missing_cst['1Day'] = pd.DataFrame(
                tmp_cst, index=tmp_tickers)

    def add_strategy(self, strategy):
        """
        Add the strategy and set the settings
        """
        self._strategy = strategy
        # create an output instance for the strategy report
        root = os.path.join(os.getcwd(), '..', 'report')
        fname = self._strategy.name + ".report"
        self.o = Output(root, fname)
        # check whether repot file has been created
        if self.o.ReportFileExist == False:
            print(
                "A 'report' folder should be provided in the parent directory of current directory."
            )

    def run(self):
        print('Running back test for the trading system..')
        self.o.report(' Blotter '.center(80, '='))
        try:
            self._strategy.compute_trading_points(self.stocks, self.szTimeAxis,
                                                  self.n_ahead)
        except BidTooLow:
            self.o.report('[Abort] Your bid was too low for one board lot.')
            self.o.report('Try increaing initial capital or buying position.')
            return
        except NotEnoughMoney:
            self.o.report('[Abort] No enough money to continue.')
            return
        except CanNotSplitShare:
            self.o.report(
                '[Abort] You only have one share currently and can not be split.'
            )
            self.o.report(
                'Examine your strategy or try set ratio in sell() to 1.')
            return
        except SellError:
            self.o.report('[Abort] Part of your sale does not exist.')
            self.o.report('Please check your strategy.')
            return

    def buy_cash(self, code, price, datetime, cash):
        """
        Use this buy function if your want to invest with your profit  

        Args: 
            datetime(str): trading time
            cash(float): the amount of cash that you would like to bid. It should be smaller than
                        those in the account.
        Returns:
            quantity(int): the number of shares (unit: boardlot) you buy this time
            shares(int): {code: int}. a dict of the amount of current shares (unit:boardlot) in \
                            the account.
            cash(float): the amount of current cash in the account
        """
        # check
        assert (price > 0)
        assert (cash <= self._cash)
        # bid: how much money we can use to buy shares
        bid = cash
        # can we buy one board lot?
        oneboardlot = 100 * price  # One board lot is 100 shares in Chinese market
        if bid < oneboardlot:
            raise BidTooLow(need_cash=oneboardlot, bid=bid)
        if self._cash < oneboardlot:
            raise NotEnoughMoney(need_cash=oneboardlot, cash=self._cash)
        # can we buy more?
        quantity = int(math.floor(bid / oneboardlot))
        need_cash = quantity * oneboardlot  # if run to here, we must have bid >= need_cash
        while self._cash < need_cash:
            quantity = quantity - 1
            need_cash = quantity * oneboardlot
        # now buy it
        self._cash -= need_cash
        try:
            self._shares[code] += quantity
        except KeyError:
            self._shares[code] = quantity
        # commission charge deduction
        if (self.CommissionChargeRate >
                0):  # require consider commission charge
            commission_charge = max(need_cash * self.CommissionChargeRate,
                                    5)  # at least 5 yuan
        else:
            commission_charge = 0
        self._cash -= commission_charge
        # update total commission charge
        self._total_commission_charge += commission_charge
        # add transaction record
        self._records.append((datetime, self._cash, self._shares.copy()))
        # print infomation
        self.o.report('- - ' * 20)
        self.o.report("[" + str(datetime) + "] ")
        self.o.report("[stock code: {:s}]".format(code))
        self.o.report(
            "Buy {0:d} board lot shares at price {1:.2f} (per share)".format(
                quantity, price))
        self.o.report("Commission charge: {:.2f}".format(commission_charge))
        self.o.report("Account: ")
        self.o.account(self._shares, self._cash)
        # return the number of shares (unit: boardlot) you buy this time
        return quantity, self._shares, self._cash

    def buy_ratio(self, code, price, datetime, ratio):
        """
        Use this buy function if your want to invest with your profit  

        Args: 
            datetime(str): trading time
            ratio(float): the proportion of the current cash that you would like to bid. 	
                        Note that the current cash is the money in your account and would 
                        vary during the back test.
        Returns:
            quantity(int): the number of shares (unit: boardlot) you buy this time
            shares(int): {code: int}. a dict of the amount of current shares (unit:boardlot) in \
                            the account.
            cash(float): the amount of current cash in the account
        """
        # check
        assert (price > 0)
        assert ((0 < ratio) and (ratio <= 1))
        # compute how many can we buy
        bid = self._cash * ratio
        # can we buy one board lot?
        oneboardlot = 100 * price  # One board lot is 100 shares in Chinese market
        if bid < oneboardlot:
            raise BidTooLow(need_cash=oneboardlot, bid=bid)
        if self._cash < oneboardlot:
            raise NotEnoughMoney(need_cash=oneboardlot, cash=self._cash)
        # can we buy more?
        quantity = int(math.floor(bid / oneboardlot))
        need_cash = quantity * oneboardlot  # if run to here, we must have bid >= need_cash
        while self._cash < need_cash:
            quantity = quantity - 1
            need_cash = quantity * oneboardlot
        # now buy it
        self._cash -= need_cash
        try:
            self._shares[code] += quantity
        except KeyError:
            self._shares[code] = quantity
        # commission charge deduction
        if (self.CommissionChargeRate >
                0):  # require consider commission charge
            commission_charge = max(need_cash * self.CommissionChargeRate,
                                    5)  # at least 5 yuan
        else:
            commission_charge = 0
        self._cash -= commission_charge
        # update total commission charge
        self._total_commission_charge += commission_charge
        # add transaction record
        self._records.append((datetime, self._cash, self._shares.copy()))
        # print infomation
        self.o.report('- - ' * 20)
        self.o.report("[" + str(datetime) + "] ")
        self.o.report("[stock code: {:s}]".format(code))
        self.o.report(
            "Buy {0:d} board lot shares at price {1:.2f} (per share)".format(
                quantity, price))
        self.o.report("Commission charge: {:.2f}".format(commission_charge))
        self.o.report("Account: ")
        self.o.account(self._shares, self._cash)
        # return the number of shares (unit: boardlot) you buy this time
        return quantity, self._shares, self._cash

    def buy_position(self, code, price, datetime, position):
        """
        Use this buy function if you only want to invest with your capital, not profit

        Args: 
            datetime(str): trading time
            position(float): the proportion of the capital that you would like to bid. 	
                            Note that the capital is the initial money amount and is 
                            a constant once set at the beginning.
        Returns:
            quantity(int): the number of shares (unit: boardlot) you buy this time
            shares(int): {code: int}. a dict of the amount of current shares (unit:boardlot) in \
                            the account.
            cash(float): the amount of current cash in the account
        """
        # check
        assert (price > 0)
        assert ((0 < position) and (position <= 1))
        # compute how many can we buy
        bid = self._capital * position
        # can we buy one board lot?
        oneboardlot = 100 * price  # One board lot is 100 shares in Chinese market
        if bid < oneboardlot:
            raise BidTooLow(need_cash=oneboardlot, bid=bid)
        if self._cash < oneboardlot:
            raise NotEnoughMoney(need_cash=oneboardlot, cash=self._cash)
        # can we buy more?
        quantity = int(math.floor(bid / oneboardlot))
        need_cash = quantity * oneboardlot  # if run to here, we must have bid >= need_cash
        while self._cash < need_cash:
            quantity = quantity - 1
            need_cash = quantity * oneboardlot
        # now buy it
        self._cash -= need_cash
        try:
            self._shares[code] += quantity
        except KeyError:
            self._shares[code] = quantity
        # commission charge deduction
        if (self.CommissionChargeRate >
                0):  # require consider commission charge
            commission_charge = max(need_cash * self.CommissionChargeRate,
                                    5)  # at least 5 yuan
        else:
            commission_charge = 0
        self._cash -= commission_charge
        # update total commission charge
        self._total_commission_charge += commission_charge
        # add transaction record
        self._records.append((datetime, self._cash, self._shares.copy()))
        # print infomation
        self.o.report('- - ' * 20)
        self.o.report("[" + str(datetime) + "] ")
        self.o.report("[stock code: {:s}]".format(code))
        self.o.report(
            "Buy {0:d} board lot shares at price {1:.2f} (per share)".format(
                quantity, price))
        self.o.report("Commission charge: {:.2f}".format(commission_charge))
        self.o.report("Account: ")
        self.o.account(self._shares, self._cash)
        # return the number of shares (unit: boardlot) you buy this time
        # and the the amount of current cash and shares (unit:boardlot) in the account
        return quantity, self._shares, self._cash

    def sell(self, code, price, datetime, quantity):
        """
        Args: 
            datetime(str): trading time
            quantity(int): the number of shares (unit: boardlot) you want to sell	
        Returns:
            shares(dict): {code: int}. a dict of the amount of current shares (unit:boardlot) in \
                            the account.
            cash(float): the amount of current cash in the account
        """
        # check
        assert (price > 0)
        assert (quantity >= 1)
        # now sell it
        income = quantity * price * 100
        self._cash += income
        try:
            self._shares[code] -= quantity
            if quantity < 0:
                raise SellError
        except KeyError:
            raise SellError
        # commission charge deduction
        if (self.CommissionChargeRate >
                0):  # require consider commission charge
            commission_charge = max(income * self.CommissionChargeRate,
                                    5)  # at least 5 yuan
        else:
            commission_charge = 0
        self._cash -= commission_charge
        # stamp tax deduction
        stamp_tax = income * self.StampTaxRate
        self._cash -= stamp_tax
        # update total commission charge
        self._total_commission_charge += commission_charge
        # update total stamp tax
        self._total_stamp_tax += stamp_tax
        # add transaction record
        self._records.append((datetime, self._cash, self._shares.copy()))
        # print infomation
        self.o.report('- - ' * 20)
        self.o.report("[" + str(datetime) + "] ")
        self.o.report("[stock code: {:s}]".format(code))
        self.o.report("Sell {0:d} board lot shares at price {1:.2f} (per share)".format(quantity, \
                                                                                            price))
        self.o.report("Commission charge: {0:.2f},    Stamp Tax: {1:.2f}".format(commission_charge,\
                                                                                         stamp_tax))
        self.o.report("Account: ")
        self.o.account(self._shares, self._cash)
        return self._shares, self._cash

    def report(self):
        # operate: reportfile.seek(0)
        # We seek(0) the reportfile because we want these following statistics and performance results be
        # written at the beginning of the reportfile (we have left space for these contents in the file)
        self.o.seek(0)
        # now start writing report
        title = " Back Test Report "
        self.o.report(title.center(80, '='))
        dt_start, dt_end = self.timescope[0], self.timescope[1]
        self.o.report("Strategy: '{:s}'".format(self._strategy.name))
        self.o.report(self._strategy.__doc__)
        self.o.report("Time Scope: from {0:s} to {1:s}".format(
            dt_start, dt_end))
        self.o.report("Trading stocks: " + ",".join(
            self.trading_stocks))  # write down the stocks that had been traded
        self.o.report("Initial capital in account: cash[{:.2f}]".format(
            self._capital))
        self.o.report("Final equity in account: ")
        self.o.account(self._shares, self._cash)

        # create equity curve data
        print("Creating equity curve data..")
        # see whether there is any transaction, if not, why bother
        if len(self._records) == 1:  # that means no transaction
            return
        # so there is some transactions. start creating equity curve
        # compute and store the state of the equity
        equity = []
        maxpeak = 0  # store the largest floating equity
        max_withdraw_ratio = 0  # store the largest withdraw ratio
        mwr_dt_start = None  # store the start datetime of maximum withdraw ratio
        mwr_dt_end = None  # store the end datetime of maximum withdraw ratio
        cur_ticker, cur_cash, cur_shares = self._records.pop(0)
        next_ticker, next_cash, next_shares = self._records.pop(0)
        next_ticker = datetime.strptime(next_ticker,
                                        "%Y-%m-%d %H:%M:%S")  # datetime
        for ticker in self.szTimeAxis:
            ticker = datetime.strptime(str(ticker),
                                       "%Y-%m-%d %H:%M:%S")  # datetime
            if ticker < next_ticker:
                floating_equity = cur_cash
                for code in cur_shares:
                    try:
                        price = self.stocks[code].cst['1Day'].at[ticker,
                                                                 'close']
                    except KeyError:
                        price = self.stocks[code].missing_cst['1Day'].at[
                            ticker, 'close']
                    cur_1boardlot_price = price * 100
                    floating_equity += cur_shares[code] * cur_1boardlot_price
                equity.append(floating_equity)
            else:
                cur_ticker, cur_cash, cur_shares = next_ticker, next_cash, next_shares
                try:
                    next_ticker, next_cash, next_shares = self._records.pop(0)
                except IndexError:  # when reach the end of _records
                    next_ticker = self.timescope[1]
                next_ticker = datetime.strptime(
                    next_ticker, "%Y-%m-%d %H:%M:%S")  # datetime
                floating_equity = cur_cash
                for code in cur_shares:
                    try:
                        price = self.stocks[code].cst['1Day'].at[ticker,
                                                                 'close']
                    except KeyError:
                        price = self.stocks[code].missing_cst['1Day'].at[
                            ticker, 'close']
                    cur_1boardlot_price = price * 100
                    floating_equity += cur_shares[code] * cur_1boardlot_price
                equity.append(floating_equity)
            # update maxpeak, mwr_dt_start, mwr_dt_end
            if floating_equity > maxpeak:
                maxpeak = floating_equity
                temp_mwr_dt_start = str(ticker)
            else:
                withdraw = maxpeak - floating_equity
                # update maximum withdraw ratio
                if withdraw / maxpeak > max_withdraw_ratio:
                    max_withdraw_ratio = withdraw / maxpeak
                    mwr_dt_start, mwr_dt_end = temp_mwr_dt_start, str(ticker)
        # continue writing report:
        self.o.report("Final equity convert into cash: {:.2f}".format(
            equity[-1]))
        profit = equity[-1] - equity[0]
        self.o.report("Profit: {:.2f}".format(profit))
        rate_of_return = profit / self._capital
        self.o.report("Rate of return: {:.2f}%".format(rate_of_return * 100))
        # get time span
        dt_start = datetime.strptime(str(dt_start),
                                     "%Y-%m-%d %H:%M:%S")  # datetime
        dt_end = datetime.strptime(str(dt_end),
                                   "%Y-%m-%d %H:%M:%S")  # datetime
        time_span_days = (dt_end - dt_start).days
        time_span_years = time_span_days / 365.0
        annualized_return = math.exp(math.log(1 + rate_of_return) / 19) - 1
        self.o.report("Annualized Return (compound interest): {:.2f}%".format(
            annualized_return * 100))
        self.o.report("Maximum withdraw ratio: {:.2f}%".format(
            max_withdraw_ratio * 100))
        self.o.report(
            "The start datetime of maximum withdraw ratio: {:s}".format(
                mwr_dt_start))
        self.o.report(
            "The end datetime of maximum withdraw ratio: {:s}".format(
                mwr_dt_end))
        self.o.report("Commission Charge: {:.2f}".format(
            self._total_commission_charge))
        self.o.report("Stamp Tax: {:.2f}".format(self._total_stamp_tax))
        # Done writing report
        self.o.close()
        # return df_equity(pd.DataFrame)
        df_equity = pd.DataFrame({
            'equity': equity,
            'sz': self.sz_daily_close
        },
                                 index=self.szTimeAxis)
        return df_equity
Exemple #4
0
class ExecuteUnit(object):
    """
    Execute unit of a strategy.
    One execute unit can only load one strategy, and the period \
    candlestick data should all belong to one specific stock. 
    """

    def __init__(self, pstocks, dt_start, dt_end, n_ahead, capital, StampTaxRate, CommissionChargeRate):
        self._strategy = None
        # actual timescope of the back test, depending on the user setting and the available data
        self.timescope = None
        # number of extra daily data for computation (ahead of start datatime)
        # set in the main program
        self.n_ahead = n_ahead
        # an output class instance for the report file
        self.o = None
        # check whether datetime is proper
        assert dt_start < dt_end
        # is_Day_cst: to check whether there is at least one daily candlestick data in pstocks
        is_Day_cst_exist = False
        # load candlestick(cst) data
        # .stocks = {code(str): PStock,}
        self.stocks = {}
        for pstock in pstocks:
            # get info from filename
            # .ID(class StockID), .period(str)
            ID, period = parse_cst_filename(pstock)
            if period == "1Day":
                is_Day_cst_exist = True
            if ID.code not in self.stocks:
                self.stocks[ID.code] = PStock(pstock, dt_start, dt_end, n_ahead)
            else:
                self.stocks[ID.code].load_cst(pstock, dt_start, dt_end, n_ahead)
        # remind user
        assert is_Day_cst_exist, "At least one item in pstocks should be daily candlestick data"
        # for convenience, store sz cst information as class PStock
        self.sz = PStock("000001.sz-1Day", dt_start, dt_end, n_ahead)
        # for convenience, store dayily close price of sz
        self.sz_daily_close = self.sz.cst["1Day"]["close"].values[n_ahead:]
        # get sz daily time axis
        self.szTimeAxis = self.sz.cst["1Day"].index[n_ahead:]  # pd.DataFrame.index
        # find missing daily candlestick data and fill them up in .stocks[code].missing_cst['1Day']
        # as pd.DataFrame
        self.fill_missing_cst()
        # read candlestick data timescope from sz
        self.timescope = str(self.szTimeAxis[0]), str(self.szTimeAxis[-1])
        # initial capital to invest, a constant once being set
        self._capital = capital
        # cash in the account, changing during the back test
        self._cash = capital
        # stock shares in the account, changing during the back test
        self._shares = {}  # ._shares: {code(str): quantity(int)}
        # records. add one record after each transaction(buy or sell)
        # _records = [(datetime(str), cash, shares),]
        self._records = []
        self._records.append((self.timescope[0], self._cash, self._shares.copy()))
        # set stamp tax rate and commission charge rate
        self.StampTaxRate = StampTaxRate
        self.CommissionChargeRate = CommissionChargeRate
        # to store total commission charge in the back test
        self._total_commission_charge = 0
        # to store total stamp tax in the back test
        self._total_stamp_tax = 0
        # trading_stocks: To store the stocks that are traded. Note that although have been loaded,
        #                 some stocks in self.stocks may not been traded in user's strategy.
        self.trading_stocks = set()

    def fill_missing_cst(self):
        """ 
        Find missing daily candlestick data and fill them up 
        in .stocks[code].missing_cst['1Day'] as pd.DataFrame 
        """
        for code in self.stocks.keys():
            # get pd.DataFrame.columns of cst data
            columns = self.stocks[code].cst["1Day"].columns
            # tmp_cst, tmp_tickers: to store missing daily candlestick data
            tmp_cst = {column: [] for column in columns}
            tmp_tickers = []
            # clear ivar: last_normal_cst at the beginning of each loop
            last_normal_cst = None
            # start to search missing cst data
            atbegin_flag = True
            for ticker in self.szTimeAxis:
                if atbegin_flag:  # if missing cst data at the beginning, fill up with zeros
                    try:
                        last_normal_cst = self.stocks[code].cst["1Day"].ix[ticker]
                    except KeyError:
                        tmp_tickers.append(ticker)
                        for column in columns:
                            tmp_cst[column].append(0.0)
                    else:
                        atbegin_flag = False
                else:
                    try:
                        last_normal_cst = self.stocks[code].cst["1Day"].ix[ticker]
                    except KeyError:
                        tmp_tickers.append(ticker)
                        for column in columns:
                            tmp_cst[column].append(last_normal_cst[column])
                    else:
                        pass
            # store missing cst as pd.DataFrame
            self.stocks[code].missing_cst["1Day"] = pd.DataFrame(tmp_cst, index=tmp_tickers)

    def add_strategy(self, strategy):
        """
        Add the strategy and set the settings
        """
        self._strategy = strategy
        # create an output instance for the strategy report
        root = os.path.join(os.getcwd(), "..", "report")
        fname = self._strategy.name + ".report"
        self.o = Output(root, fname)
        # check whether repot file has been created
        if self.o.ReportFileExist == False:
            print("A 'report' folder should be provided in the parent directory of current directory.")

    def run(self):
        print("Running back test for the trading system..")
        self.o.report(" Blotter ".center(80, "="))
        try:
            self._strategy.compute_trading_points(self.stocks, self.szTimeAxis, self.n_ahead)
        except BidTooLow:
            self.o.report("[Abort] Your bid was too low for one board lot.")
            self.o.report("Try increaing initial capital or buying position.")
            return
        except NotEnoughMoney:
            self.o.report("[Abort] No enough money to continue.")
            return
        except CanNotSplitShare:
            self.o.report("[Abort] You only have one share currently and can not be split.")
            self.o.report("Examine your strategy or try set ratio in sell() to 1.")
            return
        except SellError:
            self.o.report("[Abort] Part of your sale does not exist.")
            self.o.report("Please check your strategy.")
            return

    def buy_cash(self, code, price, datetime, cash):
        """
        Use this buy function if your want to invest with your profit  

        Args: 
            datetime(str): trading time
            cash(float): the amount of cash that you would like to bid. It should be smaller than
                        those in the account.
        Returns:
            quantity(int): the number of shares (unit: boardlot) you buy this time
            shares(int): {code: int}. a dict of the amount of current shares (unit:boardlot) in \
                            the account.
            cash(float): the amount of current cash in the account
        """
        # check
        assert price > 0
        assert cash <= self._cash
        # bid: how much money we can use to buy shares
        bid = cash
        # can we buy one board lot?
        oneboardlot = 100 * price  # One board lot is 100 shares in Chinese market
        if bid < oneboardlot:
            raise BidTooLow(need_cash=oneboardlot, bid=bid)
        if self._cash < oneboardlot:
            raise NotEnoughMoney(need_cash=oneboardlot, cash=self._cash)
        # can we buy more?
        quantity = int(math.floor(bid / oneboardlot))
        need_cash = quantity * oneboardlot  # if run to here, we must have bid >= need_cash
        while self._cash < need_cash:
            quantity = quantity - 1
            need_cash = quantity * oneboardlot
        # now buy it
        self._cash -= need_cash
        try:
            self._shares[code] += quantity
        except KeyError:
            self._shares[code] = quantity
        # commission charge deduction
        if self.CommissionChargeRate > 0:  # require consider commission charge
            commission_charge = max(need_cash * self.CommissionChargeRate, 5)  # at least 5 yuan
        else:
            commission_charge = 0
        self._cash -= commission_charge
        # update total commission charge
        self._total_commission_charge += commission_charge
        # add transaction record
        self._records.append((datetime, self._cash, self._shares.copy()))
        # print infomation
        self.o.report("- - " * 20)
        self.o.report("[" + str(datetime) + "] ")
        self.o.report("[stock code: {:s}]".format(code))
        self.o.report("Buy {0:d} board lot shares at price {1:.2f} (per share)".format(quantity, price))
        self.o.report("Commission charge: {:.2f}".format(commission_charge))
        self.o.report("Account: ")
        self.o.account(self._shares, self._cash)
        # return the number of shares (unit: boardlot) you buy this time
        return quantity, self._shares, self._cash

    def buy_ratio(self, code, price, datetime, ratio):
        """
        Use this buy function if your want to invest with your profit  

        Args: 
            datetime(str): trading time
            ratio(float): the proportion of the current cash that you would like to bid. 	
                        Note that the current cash is the money in your account and would 
                        vary during the back test.
        Returns:
            quantity(int): the number of shares (unit: boardlot) you buy this time
            shares(int): {code: int}. a dict of the amount of current shares (unit:boardlot) in \
                            the account.
            cash(float): the amount of current cash in the account
        """
        # check
        assert price > 0
        assert (0 < ratio) and (ratio <= 1)
        # compute how many can we buy
        bid = self._cash * ratio
        # can we buy one board lot?
        oneboardlot = 100 * price  # One board lot is 100 shares in Chinese market
        if bid < oneboardlot:
            raise BidTooLow(need_cash=oneboardlot, bid=bid)
        if self._cash < oneboardlot:
            raise NotEnoughMoney(need_cash=oneboardlot, cash=self._cash)
        # can we buy more?
        quantity = int(math.floor(bid / oneboardlot))
        need_cash = quantity * oneboardlot  # if run to here, we must have bid >= need_cash
        while self._cash < need_cash:
            quantity = quantity - 1
            need_cash = quantity * oneboardlot
        # now buy it
        self._cash -= need_cash
        try:
            self._shares[code] += quantity
        except KeyError:
            self._shares[code] = quantity
        # commission charge deduction
        if self.CommissionChargeRate > 0:  # require consider commission charge
            commission_charge = max(need_cash * self.CommissionChargeRate, 5)  # at least 5 yuan
        else:
            commission_charge = 0
        self._cash -= commission_charge
        # update total commission charge
        self._total_commission_charge += commission_charge
        # add transaction record
        self._records.append((datetime, self._cash, self._shares.copy()))
        # print infomation
        self.o.report("- - " * 20)
        self.o.report("[" + str(datetime) + "] ")
        self.o.report("[stock code: {:s}]".format(code))
        self.o.report("Buy {0:d} board lot shares at price {1:.2f} (per share)".format(quantity, price))
        self.o.report("Commission charge: {:.2f}".format(commission_charge))
        self.o.report("Account: ")
        self.o.account(self._shares, self._cash)
        # return the number of shares (unit: boardlot) you buy this time
        return quantity, self._shares, self._cash

    def buy_position(self, code, price, datetime, position):
        """
        Use this buy function if you only want to invest with your capital, not profit

        Args: 
            datetime(str): trading time
            position(float): the proportion of the capital that you would like to bid. 	
                            Note that the capital is the initial money amount and is 
                            a constant once set at the beginning.
        Returns:
            quantity(int): the number of shares (unit: boardlot) you buy this time
            shares(int): {code: int}. a dict of the amount of current shares (unit:boardlot) in \
                            the account.
            cash(float): the amount of current cash in the account
        """
        # check
        assert price > 0
        assert (0 < position) and (position <= 1)
        # compute how many can we buy
        bid = self._capital * position
        # can we buy one board lot?
        oneboardlot = 100 * price  # One board lot is 100 shares in Chinese market
        if bid < oneboardlot:
            raise BidTooLow(need_cash=oneboardlot, bid=bid)
        if self._cash < oneboardlot:
            raise NotEnoughMoney(need_cash=oneboardlot, cash=self._cash)
        # can we buy more?
        quantity = int(math.floor(bid / oneboardlot))
        need_cash = quantity * oneboardlot  # if run to here, we must have bid >= need_cash
        while self._cash < need_cash:
            quantity = quantity - 1
            need_cash = quantity * oneboardlot
        # now buy it
        self._cash -= need_cash
        try:
            self._shares[code] += quantity
        except KeyError:
            self._shares[code] = quantity
        # commission charge deduction
        if self.CommissionChargeRate > 0:  # require consider commission charge
            commission_charge = max(need_cash * self.CommissionChargeRate, 5)  # at least 5 yuan
        else:
            commission_charge = 0
        self._cash -= commission_charge
        # update total commission charge
        self._total_commission_charge += commission_charge
        # add transaction record
        self._records.append((datetime, self._cash, self._shares.copy()))
        # print infomation
        self.o.report("- - " * 20)
        self.o.report("[" + str(datetime) + "] ")
        self.o.report("[stock code: {:s}]".format(code))
        self.o.report("Buy {0:d} board lot shares at price {1:.2f} (per share)".format(quantity, price))
        self.o.report("Commission charge: {:.2f}".format(commission_charge))
        self.o.report("Account: ")
        self.o.account(self._shares, self._cash)
        # return the number of shares (unit: boardlot) you buy this time
        # and the the amount of current cash and shares (unit:boardlot) in the account
        return quantity, self._shares, self._cash

    def sell(self, code, price, datetime, quantity):
        """
        Args: 
            datetime(str): trading time
            quantity(int): the number of shares (unit: boardlot) you want to sell	
        Returns:
            shares(dict): {code: int}. a dict of the amount of current shares (unit:boardlot) in \
                            the account.
            cash(float): the amount of current cash in the account
        """
        # check
        assert price > 0
        assert quantity >= 1
        # now sell it
        income = quantity * price * 100
        self._cash += income
        try:
            self._shares[code] -= quantity
            if quantity < 0:
                raise SellError
        except KeyError:
            raise SellError
        # commission charge deduction
        if self.CommissionChargeRate > 0:  # require consider commission charge
            commission_charge = max(income * self.CommissionChargeRate, 5)  # at least 5 yuan
        else:
            commission_charge = 0
        self._cash -= commission_charge
        # stamp tax deduction
        stamp_tax = income * self.StampTaxRate
        self._cash -= stamp_tax
        # update total commission charge
        self._total_commission_charge += commission_charge
        # update total stamp tax
        self._total_stamp_tax += stamp_tax
        # add transaction record
        self._records.append((datetime, self._cash, self._shares.copy()))
        # print infomation
        self.o.report("- - " * 20)
        self.o.report("[" + str(datetime) + "] ")
        self.o.report("[stock code: {:s}]".format(code))
        self.o.report("Sell {0:d} board lot shares at price {1:.2f} (per share)".format(quantity, price))
        self.o.report("Commission charge: {0:.2f},    Stamp Tax: {1:.2f}".format(commission_charge, stamp_tax))
        self.o.report("Account: ")
        self.o.account(self._shares, self._cash)
        return self._shares, self._cash

    def report(self):
        # operate: reportfile.seek(0)
        # We seek(0) the reportfile because we want these following statistics and performance results be
        # written at the beginning of the reportfile (we have left space for these contents in the file)
        self.o.seek(0)
        # now start writing report
        title = " Back Test Report "
        self.o.report(title.center(80, "="))
        dt_start, dt_end = self.timescope[0], self.timescope[1]
        self.o.report("Strategy: '{:s}'".format(self._strategy.name))
        self.o.report(self._strategy.__doc__)
        self.o.report("Time Scope: from {0:s} to {1:s}".format(dt_start, dt_end))
        self.o.report("Trading stocks: " + ",".join(self.trading_stocks))  # write down the stocks that had been traded
        self.o.report("Initial capital in account: cash[{:.2f}]".format(self._capital))
        self.o.report("Final equity in account: ")
        self.o.account(self._shares, self._cash)

        # create equity curve data
        print("Creating equity curve data..")
        # see whether there is any transaction, if not, why bother
        if len(self._records) == 1:  # that means no transaction
            return
        # so there is some transactions. start creating equity curve
        # compute and store the state of the equity
        equity = []
        maxpeak = 0  # store the largest floating equity
        max_withdraw_ratio = 0  # store the largest withdraw ratio
        mwr_dt_start = None  # store the start datetime of maximum withdraw ratio
        mwr_dt_end = None  # store the end datetime of maximum withdraw ratio
        cur_ticker, cur_cash, cur_shares = self._records.pop(0)
        next_ticker, next_cash, next_shares = self._records.pop(0)
        next_ticker = datetime.strptime(next_ticker, "%Y-%m-%d %H:%M:%S")  # datetime
        for ticker in self.szTimeAxis:
            ticker = datetime.strptime(str(ticker), "%Y-%m-%d %H:%M:%S")  # datetime
            if ticker < next_ticker:
                floating_equity = cur_cash
                for code in cur_shares:
                    try:
                        price = self.stocks[code].cst["1Day"].at[ticker, "close"]
                    except KeyError:
                        price = self.stocks[code].missing_cst["1Day"].at[ticker, "close"]
                    cur_1boardlot_price = price * 100
                    floating_equity += cur_shares[code] * cur_1boardlot_price
                equity.append(floating_equity)
            else:
                cur_ticker, cur_cash, cur_shares = next_ticker, next_cash, next_shares
                try:
                    next_ticker, next_cash, next_shares = self._records.pop(0)
                except IndexError:  # when reach the end of _records
                    next_ticker = self.timescope[1]
                next_ticker = datetime.strptime(next_ticker, "%Y-%m-%d %H:%M:%S")  # datetime
                floating_equity = cur_cash
                for code in cur_shares:
                    try:
                        price = self.stocks[code].cst["1Day"].at[ticker, "close"]
                    except KeyError:
                        price = self.stocks[code].missing_cst["1Day"].at[ticker, "close"]
                    cur_1boardlot_price = price * 100
                    floating_equity += cur_shares[code] * cur_1boardlot_price
                equity.append(floating_equity)
            # update maxpeak, mwr_dt_start, mwr_dt_end
            if floating_equity > maxpeak:
                maxpeak = floating_equity
                temp_mwr_dt_start = str(ticker)
            else:
                withdraw = maxpeak - floating_equity
                # update maximum withdraw ratio
                if withdraw / maxpeak > max_withdraw_ratio:
                    max_withdraw_ratio = withdraw / maxpeak
                    mwr_dt_start, mwr_dt_end = temp_mwr_dt_start, str(ticker)
        # continue writing report:
        self.o.report("Final equity convert into cash: {:.2f}".format(equity[-1]))
        profit = equity[-1] - equity[0]
        self.o.report("Profit: {:.2f}".format(profit))
        rate_of_return = profit / self._capital
        self.o.report("Rate of return: {:.2f}%".format(rate_of_return * 100))
        # get time span
        dt_start = datetime.strptime(str(dt_start), "%Y-%m-%d %H:%M:%S")  # datetime
        dt_end = datetime.strptime(str(dt_end), "%Y-%m-%d %H:%M:%S")  # datetime
        time_span_days = (dt_end - dt_start).days
        time_span_years = time_span_days / 365.0
        annualized_return = math.exp(math.log(1 + rate_of_return) / 19) - 1
        self.o.report("Annualized Return (compound interest): {:.2f}%".format(annualized_return * 100))
        self.o.report("Maximum withdraw ratio: {:.2f}%".format(max_withdraw_ratio * 100))
        self.o.report("The start datetime of maximum withdraw ratio: {:s}".format(mwr_dt_start))
        self.o.report("The end datetime of maximum withdraw ratio: {:s}".format(mwr_dt_end))
        self.o.report("Commission Charge: {:.2f}".format(self._total_commission_charge))
        self.o.report("Stamp Tax: {:.2f}".format(self._total_stamp_tax))
        # Done writing report
        self.o.close()
        # return df_equity(pd.DataFrame)
        df_equity = pd.DataFrame({"equity": equity, "sz": self.sz_daily_close}, index=self.szTimeAxis)
        return df_equity