def __init__( self, input_shape: Tuple[int, ...], trading_fraction: int = 10, trading_assets: int = 1, # later we want the bot to trade one of multiple possible assets allow_short: bool = False, stop_if_lost: float = None, initial_capital: float = 100000, commission=lambda size: 0.025): super().__init__( MultiDiscrete([trading_assets, trading_fraction]) if trading_assets > 1 else Discrete(trading_fraction + 1), Box(low=-1, high=1, shape=input_shape) ) # FIXME what shape? we also need historic trades? self.trading_fraction = trading_fraction self.initial_capital = initial_capital self.commission = commission self.stop_if_lost = stop_if_lost self.allow_short = allow_short if allow_short and (trading_fraction % 2) != 0: _log.warning('short trades expect even nr of trading fraction') # eventually do not serialize .. self.trade_log = StreamingTransactionLog() self.current_net = 0
def ta_backtest(signal: Typing.PatchedDataFrame, prices: Typing.PatchedPandas, action: Callable[[pd.Series], Tuple[int, float]], slippage: Callable[[float], float] = lambda _: 0): if has_indexed_columns(signal): assert len(signal.columns) == len( prices.columns), "Signal and Prices need the same shape!" res = pd.DataFrame({}, index=signal.index, columns=pd.MultiIndex.from_product([[], []])) for i in range(len(signal.columns)): df = ta_backtest(signal[signal.columns[i]], prices[prices.columns[i]], action, slippage) top_level_name = ",".join(prices.columns[i]) if isinstance( prices.columns[i], tuple) else prices.columns[i] df.columns = pd.MultiIndex.from_product([[top_level_name], df.columns.to_list()]) res = res.join(df) return res assert isinstance(prices, pd.Series), "prices need to be a series!" trades = StreamingTransactionLog() def trade_log_action(row): direction_amount = action(row) if isinstance(direction_amount, tuple): trades.perform_action(*direction_amount) else: trades.rebalance(float(direction_amount)) signal.to_frame().apply(trade_log_action, axis=1, raw=True) return trades.evaluate(prices.rename("price"), slippage)
def calculate_performance(self, initial_capital=100, prediction_name="0", target_name="Close"): log = StreamingTransactionLog() self.df[PREDICTION_COLUMN_NAME, prediction_name].apply( lambda action: log.rebalance(action * initial_capital)) perf = log.evaluate(self.df[TARGET_COLUMN_NAME, target_name].rename("prices")) return perf
def reset(self) -> np.ndarray: self.trade_log = StreamingTransactionLog() self.current_net = 0 return super().reset()
class TradingAgentGym(ReinforcementModel.DataFrameGym): def __init__( self, input_shape: Tuple[int, ...], trading_fraction: int = 10, trading_assets: int = 1, # later we want the bot to trade one of multiple possible assets allow_short: bool = False, stop_if_lost: float = None, initial_capital: float = 100000, commission=lambda size: 0.025): super().__init__( MultiDiscrete([trading_assets, trading_fraction]) if trading_assets > 1 else Discrete(trading_fraction + 1), Box(low=-1, high=1, shape=input_shape) ) # FIXME what shape? we also need historic trades? self.trading_fraction = trading_fraction self.initial_capital = initial_capital self.commission = commission self.stop_if_lost = stop_if_lost self.allow_short = allow_short if allow_short and (trading_fraction % 2) != 0: _log.warning('short trades expect even nr of trading fraction') # eventually do not serialize .. self.trade_log = StreamingTransactionLog() self.current_net = 0 def reset(self) -> np.ndarray: self.trade_log = StreamingTransactionLog() self.current_net = 0 return super().reset() def interpret_action(self, action, idx: int, features: np.ndarray, labels: np.ndarray, targets: np.ndarray, weights: np.ndarray) -> float: if self.allow_short: # 0 - 10 -> -10 - 10 action -= int(self.trading_fraction / 2) * 2 # we convert the action in to a target balance we pass to the transaction log balance = action / self.trading_fraction / float( targets) * self.initial_capital return balance def take_action(self, action, idx: int, features: np.ndarray, labels: np.ndarray, targets: np.ndarray, weights: np.ndarray) -> float: # skip the very first bar if idx <= 0: self.trade_log.rebalance(0) return 0 # we use a n/fractions as target balance -> 10 shares 20,... shares, ... balance = action / self.trading_fraction * self.initial_capital self.trade_log.rebalance(balance) # new we need to evaluate the portfolio performance # where the targets are the prices prices = pd.Series(self.train[2][:idx + 1].ravel(), name="prices") perf = self.trade_log.evaluate(prices, self.commission) self.current_net = perf["net"].iloc[-1] if self.stop_if_lost is not None and self.current_net < self.initial_capital * ( 1 - self.stop_if_lost): raise StopIteration(f"lost more then {self.stop_if_lost}%") return self.calculate_trade_reward(perf) def calculate_trade_reward(self, portfolio_performance_log) -> float: return portfolio_performance_log["net"].iloc[-1] def next_observation(self, idx: int, features: np.ndarray, labels: np.ndarray, targets: np.ndarray, weights: np.ndarray) -> np.ndarray: # currently returns only the features, but we also want to return some net worth, history, ... # for CNNs input must be [0, 255] and channel last, this means we need to reshape our feature space # return features.swapaxes(0, 1).swapaxes(1, 2) * 255 pass def render(self, mode='human'): if mode == 'system': print(self.current_net) elif mode == 'notebook': # TODO plot something using matplotlib pass elif mode == 'human': # TODO plot something using matplotlib pass
def test_rebalance(self): df = DF_TEST[-10:].copy() df["Price"] = range(1, 11) trans = StreamingTransactionLog() trans.rebalance(0.1) trans.rebalance(0) trans.rebalance(0.5) trans.rebalance(0.6) trans.rebalance(0.4) trans.rebalance(0.2) trans.rebalance(-0.2) trans.rebalance(-0.3) trans.rebalance(-0.1) trans.rebalance(0) values = trans.evaluate(df["Price"]) print(values) self.assertEqual(0, values.iloc[-1, 1]) np.testing.assert_almost_equal(1.2, values.iloc[-1, -1])