Exemple #1
0
class BackTest:
    def __init__(
        self,
        strategy: typing.Type[Strategy],
        user_id: str = "",
        password: str = "",
        stock_id: str = "",
        start_date: str = "",
        end_date: str = "",
        trader_fund: float = 0,
        fee: float = 0.001425,
    ):
        if not (isinstance(strategy, type) and issubclass(strategy, Strategy)):
            raise TypeError("`strategy` must be a Strategy sub-type")

        self.stock_id = stock_id
        self.start_date = start_date
        self.end_date = end_date
        self.trader_fund = trader_fund
        self.fee = fee
        underlying_type = get_asset_underlying_type(stock_id)
        self.tax = get_underlying_trading_tax(underlying_type)
        self.trader = Trader(
            stock_id=stock_id,
            hold_volume=0,
            hold_cost=0,
            trader_fund=trader_fund,
            fee=self.fee,
            tax=self.tax,
        )
        self.user_id = user_id
        self.password = password
        self.strategy = strategy
        self._results = pd.DataFrame()
        self._final_stats = pd.Series()

    def __init_base_data(self) -> pd.DataFrame:
        # FIXME: some stock_id do not have div
        self.base_data = FinData(
            dataset="TaiwanStockPrice",
            select=self.stock_id,
            date=self.start_date,
            end_date=self.end_date,
            user_id=self.user_id,
            password=self.password,
        )
        StockDividend = FinData(
            dataset="StockDividend",
            select=self.stock_id,
            date=self.start_date,
            end_date=self.end_date,
            user_id=self.user_id,
            password=self.password,
        )
        if not StockDividend.empty:
            cash_div = StockDividend[
                [
                    "stock_id",
                    "CashExDividendTradingDate",
                    "CashEarningsDistribution",
                ]
            ].rename(columns={"CashExDividendTradingDate": "date"})
            stock_div = StockDividend[
                [
                    "stock_id",
                    "StockExDividendTradingDate",
                    "StockEarningsDistribution",
                ]
            ].rename(columns={"StockExDividendTradingDate": "date"})
            self.base_data = pd.merge(
                self.base_data,
                cash_div,
                left_on=["stock_id", "date"],
                right_on=["stock_id", "date"],
                how="left",
            ).fillna(0)
            self.base_data = pd.merge(
                self.base_data,
                stock_div,
                left_on=["stock_id", "date"],
                right_on=["stock_id", "date"],
                how="left",
            ).fillna(0)
        else:
            self.base_data["StockEarningsDistribution"] = 0
            self.base_data["CashEarningsDistribution"] = 0

    def simulate(self):
        self.__init_base_data()
        trader = self.trader
        strategy = self.strategy(trader)
        self.base_data = strategy.init(base_data=self.base_data)
        assert (
            "signal" in self.base_data.columns
        ), "Must be create signal columns in base_data"
        if not self.base_data.index.is_monotonic_increasing:
            warnings.warn(
                "Data index is not sorted in ascending order. Sorting.",
                stacklevel=2,
            )
            self.base_data = self.base_data.sort_index()
        for i in range(1, len(self.base_data)):
            # use last date to decide buy or sell or nothing
            last_date_index = i - 1
            signal = self.base_data.loc[last_date_index, "signal"]
            trade_price = self.base_data.loc[i, "open"]
            strategy.trade(signal, trade_price)
            cash_div = self.base_data.loc[i, "CashEarningsDistribution"]
            stock_div = self.base_data.loc[i, "StockEarningsDistribution"]
            self.__compute_div_income(strategy.trader, cash_div, stock_div)
            dic_value = strategy.trader.__dict__
            dic_value["date"] = self.base_data.loc[i, "date"]
            self._results = self._results.append(dic_value, ignore_index=True)

        self.__compute_final_stats()

    def __compute_div_income(self, trader, cash_div: float, stock_div: float):
        gain_stock_div = stock_div * trader.hold_volume / 10
        gain_cash = cash_div * trader.hold_volume
        origin_cost = trader.hold_cost * trader.hold_volume
        trader.hold_volume += gain_stock_div
        new_cost = origin_cost - gain_cash
        trader.hold_cost = (
            new_cost / trader.hold_volume if trader.hold_volume != 0 else 0
        )
        trader.UnrealizedProfit = round(
            (
                trader.trade_price * (1 - trader.tax - trader.fee)
                - trader.hold_cost
            )
            * trader.hold_volume,
            2,
        )
        trader.RealizedProfit += gain_cash
        trader.EverytimeProfit = trader.RealizedProfit + trader.UnrealizedProfit

    def __compute_final_stats(self):
        self._final_stats["MeanProfit"] = np.mean(
            self._results["EverytimeProfit"]
        )
        self._final_stats["MaxLoss"] = np.min(self._results["EverytimeProfit"])
        self._final_stats["FinalProfit"] = self._results[
            "EverytimeProfit"
        ].values[-1]
        self._final_stats["MeanProfitPer[%]"] = round(
            self._final_stats["MeanProfit"] / self.trader_fund * 100, 2
        )
        self._final_stats["FinalProfitPer[%]"] = round(
            self._final_stats["FinalProfit"] / self.trader_fund * 100, 2
        )
        self._final_stats["MaxLossPer[%]"] = round(
            self._final_stats["MaxLoss"] / self.trader_fund * 100, 2
        )

    def get_final_stats(self) -> pd.Series():
        return self._final_stats

    def get_results(self) -> pd.DataFrame():
        return self._results

    def plot(
        self,
        title: str = "Backtest Result",
        xlabel: str = "Time",
        ylabel: str = "Profit",
        grid: bool = True,
    ):
        try:
            import matplotlib.pyplot as plt
            import matplotlib.gridspec as gridspec
        except ImportError:
            raise ImportError("You must install matplotlib to plot importance")

        fig = plt.figure(figsize=(12, 8))
        gs = gridspec.GridSpec(4, 1, figure=fig)
        ax = fig.add_subplot(gs[:2, :])
        xpos = self._results.index
        ax.plot("UnrealizedProfit", data=self._results, marker="", alpha=0.8)
        ax.plot("RealizedProfit", data=self._results, marker="", alpha=0.8)
        ax.plot("EverytimeProfit", data=self._results, marker="", alpha=0.8)
        ax.grid(grid)
        ax.legend(loc=2)
        axx = ax.twinx()
        axx.bar(
            xpos,
            self._results["hold_volume"],
            alpha=0.2,
            label="hold_volume",
            color="pink",
        )
        axx.legend(loc=3)
        ax2 = fig.add_subplot(gs[2:, :], sharex=ax)
        ax2.plot(
            "trade_price",
            data=self._results,
            marker="",
            label="open",
            alpha=0.8,
        )
        ax2.plot(
            "hold_cost",
            data=self._results,
            marker="",
            label="hold_cost",
            alpha=0.8,
        )
        # TODO: add signal plot
        ax2.legend(loc=2)
        ax2.grid(grid)
        if title is not None:
            ax.set_title(title)
        if xlabel is not None:
            ax.set_xlabel(xlabel)
        if ylabel is not None:
            ax.set_ylabel(ylabel)
        plt.show()
Exemple #2
0
class BackTest:
    def __init__(
        self,
        user_id: str = "",
        password: str = "",
        stock_id: str = "",
        start_date: str = "",
        end_date: str = "",
        trader_fund: float = 0,
        fee: float = 0.001425,
        strategy: Strategy = None,
    ):
        self.stock_id = stock_id
        self.start_date = start_date
        self.end_date = end_date
        self.trader_fund = trader_fund
        self.fee = fee
        underlying_type = get_asset_underlying_type(stock_id)
        self.tax = get_underlying_trading_tax(underlying_type)
        self.trader = Trader(
            stock_id=stock_id,
            hold_volume=0,
            hold_cost=0,
            trader_fund=trader_fund,
            fee=self.fee,
            tax=self.tax,
        )
        self.user_id = user_id
        self.password = password
        self.strategy = strategy
        self._trade_detail = pd.DataFrame()
        self._final_stats = pd.Series()
        self.__init_base_data()

    def add_strategy(self, strategy: Strategy):
        self.strategy = strategy

    def __init_base_data(self) -> pd.DataFrame:
        # FIXME: some stock_id do not have div
        self.stock_price = FinData(
            dataset="TaiwanStockPrice",
            select=self.stock_id,
            date=self.start_date,
            end_date=self.end_date,
            user_id=self.user_id,
            password=self.password,
        )
        StockDividend = FinData(
            dataset="StockDividend",
            select=self.stock_id,
            date=self.start_date,
            end_date=self.end_date,
            user_id=self.user_id,
            password=self.password,
        )
        if not StockDividend.empty:
            cash_div = StockDividend[
                [
                    "stock_id",
                    "CashExDividendTradingDate",
                    "CashEarningsDistribution",
                ]
            ].rename(columns={"CashExDividendTradingDate": "date"})
            stock_div = StockDividend[
                [
                    "stock_id",
                    "StockExDividendTradingDate",
                    "StockEarningsDistribution",
                ]
            ].rename(columns={"StockExDividendTradingDate": "date"})
            self.stock_price = pd.merge(
                self.stock_price,
                cash_div,
                left_on=["stock_id", "date"],
                right_on=["stock_id", "date"],
                how="left",
            ).fillna(0)
            self.stock_price = pd.merge(
                self.stock_price,
                stock_div,
                left_on=["stock_id", "date"],
                right_on=["stock_id", "date"],
                how="left",
            ).fillna(0)
        else:
            self.stock_price["StockEarningsDistribution"] = 0
            self.stock_price["CashEarningsDistribution"] = 0

    def simulate(self):
        trader = self.trader
        strategy = self.strategy(
            trader, self.stock_id, self.start_date, self.end_date
        )
        self.stock_price = strategy.create_trade_sign(
            stock_price=self.stock_price
        )
        assert (
            "signal" in self.stock_price.columns
        ), "Must be create signal columns in stock_price"
        if not self.stock_price.index.is_monotonic_increasing:
            warnings.warn(
                "Data index is not sorted in ascending order. Sorting.",
                stacklevel=2,
            )
            self.stock_price = self.stock_price.sort_index()
        for i in range(1, len(self.stock_price)):
            # use last date to decide buy or sell or nothing
            last_date_index = i - 1
            signal = self.stock_price.loc[last_date_index, "signal"]
            trade_price = self.stock_price.loc[i, "open"]
            strategy.trade(signal, trade_price)
            cash_div = self.stock_price.loc[i, "CashEarningsDistribution"]
            stock_div = self.stock_price.loc[i, "StockEarningsDistribution"]
            self.__compute_div_income(strategy.trader, cash_div, stock_div)
            dic_value = strategy.trader.__dict__
            dic_value["date"] = self.stock_price.loc[i, "date"]
            dic_value["signal"] = self.stock_price.loc[i, "signal"]
            self._trade_detail = self._trade_detail.append(
                dic_value, ignore_index=True
            )

        self.__compute_final_stats()

    def __compute_div_income(self, trader, cash_div: float, stock_div: float):
        gain_stock_div = stock_div * trader.hold_volume / 10
        gain_cash = cash_div * trader.hold_volume
        origin_cost = trader.hold_cost * trader.hold_volume
        trader.hold_volume += gain_stock_div
        new_cost = origin_cost - gain_cash
        trader.hold_cost = (
            new_cost / trader.hold_volume if trader.hold_volume != 0 else 0
        )
        trader.UnrealizedProfit = round(
            (
                trader.trade_price * (1 - trader.tax - trader.fee)
                - trader.hold_cost
            )
            * trader.hold_volume,
            2,
        )
        trader.RealizedProfit += gain_cash
        trader.EverytimeProfit = trader.RealizedProfit + trader.UnrealizedProfit

    def __compute_final_stats(self):
        self._final_stats["MeanProfit"] = np.mean(
            self._trade_detail["EverytimeProfit"]
        )
        self._final_stats["MaxLoss"] = np.min(
            self._trade_detail["EverytimeProfit"]
        )
        self._final_stats["FinalProfit"] = self._trade_detail[
            "EverytimeProfit"
        ].values[-1]
        self._final_stats["MeanProfitPer"] = round(
            self._final_stats["MeanProfit"] / self.trader_fund * 100, 2
        )
        self._final_stats["FinalProfitPer"] = round(
            self._final_stats["FinalProfit"] / self.trader_fund * 100, 2
        )
        self._final_stats["MaxLossPer"] = round(
            self._final_stats["MaxLoss"] / self.trader_fund * 100, 2
        )
        # +1, calculate_Datenbr not include last day
        trade_days = (
            calculate_Datenbr(
                self._trade_detail["date"].min(),
                self._trade_detail["date"].max(),
            )
            + 1
        )
        trade_years = (trade_days + 1) / 365
        # +1, self._trade_detail wihtout contain first day
        self._final_stats["AnnualReturnPer"] = round(
            (
                (self._final_stats["FinalProfitPer"] / 100 + 1)
                ** (1 / trade_years)
                - 1
            )
            * 100,
            2,
        )
        timestep_returns = (
            self._trade_detail["EverytimeProfit"]
            - self._trade_detail["EverytimeProfit"].shift(1)
        ) / (self._trade_detail["EverytimeProfit"].shift(1) + self.trader_fund)
        stratagy_return = np.mean(timestep_returns)
        stratagy_std = np.std(timestep_returns)
        self._final_stats["AnnualSharpRatio"] = calculate_sharp_ratio(
            stratagy_return, stratagy_std
        )

    @property
    def final_stats(self) -> pd.Series():
        self._final_stats = pd.Series(
            output.final_stats(**self._final_stats.to_dict()).dict()
        )
        return self._final_stats

    @property
    def trade_detail(self) -> pd.DataFrame():
        self._trade_detail = pd.DataFrame(
            [
                output.trade_detail(**row_dict).dict()
                for row_dict in self._trade_detail.to_dict("records")
            ]
        )
        return self._trade_detail

    def plot(
        self,
        title: str = "Backtest Result",
        xlabel: str = "Time",
        ylabel: str = "Profit",
        grid: bool = True,
    ):
        try:
            import matplotlib.pyplot as plt
            import matplotlib.gridspec as gridspec
        except ImportError:
            raise ImportError("You must install matplotlib to plot importance")

        fig = plt.figure(figsize=(12, 8))
        gs = gridspec.GridSpec(4, 1, figure=fig)
        ax = fig.add_subplot(gs[:2, :])
        xpos = self._trade_detail.index
        ax.plot(
            "UnrealizedProfit", data=self._trade_detail, marker="", alpha=0.8
        )
        ax.plot("RealizedProfit", data=self._trade_detail, marker="", alpha=0.8)
        ax.plot(
            "EverytimeProfit", data=self._trade_detail, marker="", alpha=0.8
        )
        ax.grid(grid)
        ax.legend(loc=2)
        axx = ax.twinx()
        axx.bar(
            xpos,
            self._trade_detail["hold_volume"],
            alpha=0.2,
            label="hold_volume",
            color="pink",
        )
        axx.legend(loc=3)
        ax2 = fig.add_subplot(gs[2:, :], sharex=ax)
        ax2.plot(
            "trade_price",
            data=self._trade_detail,
            marker="",
            label="open",
            alpha=0.8,
        )
        ax2.plot(
            "hold_cost",
            data=self._trade_detail,
            marker="",
            label="hold_cost",
            alpha=0.8,
        )
        # TODO: add signal plot
        ax2.legend(loc=2)
        ax2.grid(grid)
        if title is not None:
            ax.set_title(title)
        if xlabel is not None:
            ax.set_xlabel(xlabel)
        if ylabel is not None:
            ax.set_ylabel(ylabel)
        plt.show()