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()
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()