def align_to(self, other): """Align to `other` by their indexes and columns. Example: ```python-repl >>> import pandas as pd >>> df1 = pd.DataFrame([[1, 2], [3, 4]], index=['x', 'y'], columns=['a', 'b']) >>> df2 = pd.DataFrame([[5, 6, 7, 8], [9, 10, 11, 12]], index=['x', 'y'], ... columns=pd.MultiIndex.from_arrays([[1, 1, 2, 2], ['a', 'b', 'a', 'b']])) >>> print(df1.vbt.align_to(df2)) 1 2 a b a b x 1 2 1 2 y 3 4 3 4 ```""" checks.assert_type(other, (pd.Series, pd.DataFrame)) obj = reshape_fns.to_2d(self._obj) other = reshape_fns.to_2d(other) aligned_index = index_fns.align_index_to(obj.index, other.index) aligned_columns = index_fns.align_index_to(obj.columns, other.columns) obj = obj.iloc[aligned_index, aligned_columns] return self.wrap_array(obj.values, index=other.index, columns=other.columns)
def apply_and_concat(self, ntimes, *args, apply_func=None, as_columns=None, **kwargs): """Apply a function n times and concatenate results into a single dataframe.""" checks.assert_not_none(apply_func) if checks.is_numba_func(apply_func): # NOTE: your apply_func must a numba-compiled function and arguments must be numba-compatible # Also NOTE: outputs of apply_func must always be 2-dimensional result = combine_fns.apply_and_concat_nb(np.asarray(self._obj), ntimes, apply_func, *args, **kwargs) else: result = combine_fns.apply_and_concat(np.asarray(self._obj), ntimes, apply_func, *args, **kwargs) # Build column hierarchy if as_columns is not None: new_columns = index_fns.combine( as_columns, reshape_fns.to_2d(self._obj).columns) else: new_columns = index_fns.tile( reshape_fns.to_2d(self._obj).columns, ntimes) return self.wrap_array(result, columns=new_columns)
def assert_same_meta(arg1, arg2): """Raise exception if `arg1` and `arg2` have different metadata.""" assert_same_type(arg1, arg2) assert_same_shape(arg1, arg2) if is_pandas(arg1) or is_pandas(arg2): assert_same_index(arg1, arg2) assert_same_columns(reshape_fns.to_2d(arg1), reshape_fns.to_2d(arg2))
def align_to(self, other): checks.assert_type(other, (pd.Series, pd.DataFrame)) obj = reshape_fns.to_2d(self._obj) other = reshape_fns.to_2d(other) aligned_index = index_fns.align_to(obj.index, other.index) aligned_columns = index_fns.align_to(obj.columns, other.columns) obj = obj.iloc[aligned_index, aligned_columns] return self.wrap_array(obj.values, index=other.index, columns=other.columns)
def repeat(self, n, as_columns=None): repeated = reshape_fns.repeat(self._obj, n, along_axis=1) if as_columns is not None: new_columns = index_fns.combine( reshape_fns.to_2d(self._obj).columns, as_columns) return self.wrap_array(repeated.values, columns=new_columns) return repeated
def update_data(self, data): """Update the data of the plot efficiently. Args: data (array_like): Data in any format that can be converted to NumPy. Must be of shape (`x_labels`, `trace_names`). Example: ```py fig = pd.Series([1, 2], index=['x', 'y'], name='a').vbt.Bar() fig.update_data([2, 1]) fig.show() ``` ![](/vectorbt/docs/img/Bar_updated.png) """ data = reshape_fns.to_2d(np.asarray(data)) checks.assert_same_shape(data, self._x_labels, axis=(0, 0)) checks.assert_same_shape(data, self._trace_names, axis=(1, 0)) # Update traces with self.batch_update(): for i, bar in enumerate(self.data): bar.y = data[:, i] if bar.marker.colorscale is not None: bar.marker.color = data[:, i]
def build_mapper(params, ts, new_columns, level_name): """Build a mapper that maps parameter values in `params` to columns in `new_columns`.""" params_mapper = np.repeat(params, len(reshape_fns.to_2d(ts).columns)) params_mapper = pd.Series(params_mapper, index=new_columns, name=level_name) return params_mapper
def __call__(self, trace_names=None, **kwargs): if trace_names is None: if checks.is_frame(self._obj) or (checks.is_series(self._obj) and self._obj.name is not None): trace_names = reshape_fns.to_2d(self._obj).columns return widgets.Histogram(trace_names=trace_names, data=self._obj.values, **kwargs)
def tile(self, n, as_columns=None): tiled = reshape_fns.tile(self._obj, n, along_axis=1) if as_columns is not None: new_columns = index_fns.combine( as_columns, reshape_fns.to_2d(self._obj).columns) return self.wrap_array(tiled.values, columns=new_columns) return tiled
def apply_and_concat_one(n, apply_func, *args, **kwargs): """For each value `i` from 0 to `n`, apply `apply_func` with arguments `*args` and `**kwargs`, and concat the results along axis 1. The result of `apply_func` must be a single 1-dim or 2-dim array. `apply_func` must accept arguments `i`, `*args` and `**kwargs`.""" return np.hstack( [reshape_fns.to_2d(apply_func(i, *args, **kwargs)) for i in range(n)])
def __call__(self, x_labels=None, y_labels=None, **kwargs): if x_labels is None: x_labels = reshape_fns.to_2d(self._obj).columns if y_labels is None: y_labels = self._obj.index return widgets.Heatmap(x_labels, y_labels, data=self._obj.values, **kwargs)
def combine_with(self, other, *args, combine_func=None, pass_2d=False, broadcast_kwargs={}, **kwargs): """Combine both using `combine_func` into a Series/DataFrame of the same shape. All arguments will be broadcasted using `vectorbt.utils.reshape_fns.broadcast` with `broadcast_kwargs`. Arguments `*args` and `**kwargs` will be directly passed to `combine_func`. If `pass_2d` is `True`, 2-dimensional NumPy arrays will be passed, otherwise as is. Example: ```python-repl >>> import pandas as pd >>> sr = pd.Series([1, 2], index=['x', 'y']) >>> df = pd.DataFrame([[3, 4], [5, 6]], index=['x', 'y'], columns=['a', 'b']) >>> print(sr.vbt.combine_with(df, combine_func=lambda x, y: x + y)) a b x 4 5 y 7 8 ```""" if isinstance(other, Base_Accessor): other = other._obj checks.assert_not_none(combine_func) if checks.is_numba_func(combine_func): # Numba requires writable arrays broadcast_kwargs = {**dict(writeable=True), **broadcast_kwargs} new_obj, new_other = reshape_fns.broadcast(self._obj, other, **broadcast_kwargs) # Optionally cast to 2d array if pass_2d: new_obj_arr = reshape_fns.to_2d(np.asarray(new_obj)) new_other_arr = reshape_fns.to_2d(np.asarray(new_other)) else: new_obj_arr = np.asarray(new_obj) new_other_arr = np.asarray(new_other) result = combine_func(new_obj_arr, new_other_arr, *args, **kwargs) return new_obj.vbt.wrap_array(result)
def __call__(self, x_labels=None, trace_names=None, **kwargs): if x_labels is None: x_labels = self._obj.index if trace_names is None: if checks.is_frame(self._obj) or (checks.is_series(self._obj) and self._obj.name is not None): trace_names = reshape_fns.to_2d(self._obj).columns return widgets.Scatter(x_labels, trace_names=trace_names, data=self._obj.values, **kwargs)
def rolling_window(self, window, n=None): cube = nb.rolling_window_nb(self.to_2d_array(), window) if n is not None: idxs = np.round(np.linspace(0, cube.shape[2]-1, n)).astype(int) cube = cube[:, :, idxs] else: idxs = np.arange(cube.shape[2]) matrix = np.hstack(cube) range_columns = pd.Index(self._obj.index[idxs], name='start_date') new_columns = index_fns.combine(range_columns, reshape_fns.to_2d(self._obj).columns) return pd.DataFrame(matrix, columns=new_columns)
def perform_init_checks(ts_list, output_list, param_list, mapper_list, name): """Perform checks on objects created by running or slicing an indicator.""" checks.assert_type(ts_list[0], (pd.Series, pd.DataFrame)) for ts in ts_list + output_list: checks.assert_same_meta(ts_list[0], ts) for params in param_list: checks.assert_same_shape(param_list[0], params) for mapper in mapper_list: checks.assert_type(mapper, pd.Series) checks.assert_same_index( reshape_fns.to_2d(ts_list[0]).iloc[0, :], mapper) checks.assert_type(name, str)
def perform_init_checks(ts_list, output_list, param_list, mapper_list, name): """Perform checks on objects created by running or slicing an indicator.""" for ts in ts_list: checks.assert_type(ts, (pd.Series, pd.DataFrame)) ts.vbt.timeseries.validate() for i in range(1, len(ts_list) + len(output_list)): checks.assert_same_meta((ts_list + output_list)[i - 1], (ts_list + output_list)[i]) for i in range(1, len(param_list)): checks.assert_same_shape(param_list[i - 1], param_list[i]) for mapper in mapper_list: checks.assert_type(mapper, pd.Series) checks.assert_same_index( reshape_fns.to_2d(ts_list[0]).iloc[0, :], mapper) checks.assert_type(name, str)
def update_data(self, data): """Update the data of the plot efficiently. Args: data (array_like): Data in any format that can be converted to NumPy. Must be of shape (`x_labels`, `trace_names`). """ data = reshape_fns.to_2d(np.asarray(data)) checks.assert_same_shape(data, self._x_labels, axis=(0, 0)) checks.assert_same_shape(data, self._trace_names, axis=(1, 0)) # Update traces with self.batch_update(): for i, scatter in enumerate(self.data): scatter.y = data[:, i]
def apply_and_concat(self, ntimes, *args, apply_func=None, pass_2d=False, as_columns=None, **kwargs): """Apply `apply_func` `ntimes` times and concatenate the results along columns. See `vectorbt.utils.combine_fns.apply_and_concat`. Arguments `*args` and `**kwargs` will be directly passed to `apply_func`. If `pass_2d` is `True`, 2-dimensional NumPy arrays will be passed, otherwise as is. Use `as_columns` as a top-level column level. Example: ```python-repl >>> import pandas as pd >>> df = pd.DataFrame([[3, 4], [5, 6]], index=['x', 'y'], columns=['a', 'b']) >>> print(df.vbt.apply_and_concat(3, [1, 2, 3], ... apply_func=lambda i, a, b: a * b[i], as_columns=['c', 'd', 'e'])) c d e a b a b a b x 3 4 6 8 9 12 y 5 6 10 12 15 18 ```""" checks.assert_not_none(apply_func) # Optionally cast to 2d array if pass_2d: obj_arr = reshape_fns.to_2d(np.asarray(self._obj)) else: obj_arr = np.asarray(self._obj) if checks.is_numba_func(apply_func): result = combine_fns.apply_and_concat_nb(obj_arr, ntimes, apply_func, *args, **kwargs) else: result = combine_fns.apply_and_concat(obj_arr, ntimes, apply_func, *args, **kwargs) # Build column hierarchy if as_columns is not None: new_columns = index_fns.combine_indexes(as_columns, self.columns) else: new_columns = index_fns.tile_index(self.columns, ntimes) return self.wrap_array(result, columns=new_columns)
def update_data(self, data): """Update the data of the plot efficiently. Args: data (array_like): Data in any format that can be converted to NumPy. Must be of shape (`y_labels`, `x_labels`). """ data = reshape_fns.to_2d(np.asarray(data)) checks.assert_same_shape(data, self._x_labels, axis=(1, 0)) checks.assert_same_shape(data, self._y_labels, axis=(0, 0)) # Update traces with self.batch_update(): heatmap = self.data[0] if self._horizontal: heatmap.z = data.transpose() else: heatmap.z = data
def update_data(self, data): """Update the data of the plot efficiently. Args: data (array_like): Data in any format that can be converted to NumPy. Must be of shape (any, `trace_names`). """ data = reshape_fns.to_2d(np.asarray(data)) checks.assert_same_shape(data, self._trace_names, axis=(1, 0)) # Update traces with self.batch_update(): for i, histogram in enumerate(self.data): if self._horizontal: histogram.x = None histogram.y = data[:, i] else: histogram.x = data[:, i] histogram.y = None
def from_order_func(cls, price, order_func_nb, *args, init_capital=None, fees=None, slippage=None): """Build portfolio from a custom order function. Starting with initial capital `init_capital`, at each time step, orders the number of shares returned by `order_func_nb`. Args: price (pandas_like): Price of the asset. order_func_nb (function): Function that returns the amount of shares to order. See `vectorbt.portfolio.nb.portfolio_nb`. *args: Arguments passed to `order_func_nb`. init_capital (int or float): The initial capital. fees (float or array_like): Trading fees in percentage of the value involved. slippage (float or array_like): Slippage in percentage of `price`. All array-like arguments will be broadcasted together using `vectorbt.utils.reshape_fns.broadcast` with `broadcast_kwargs`. At the end, each time series object will have the same metadata. !!! note `order_func_nb` must be Numba-compiled. Example: Portfolio value of a simple buy-and-hold strategy: ```python-repl >>> @njit ... def order_func_nb(col, i, run_cash, run_shares): ... return 10 if i == 0 else 0 >>> portfolio = vbt.Portfolio.from_order_func(price, ... order_func_nb, init_capital=100, fees=0.0025) >>> print(portfolio.cash) 2018-01-01 89.975 2018-01-02 89.975 2018-01-03 89.975 2018-01-04 89.975 2018-01-05 89.975 dtype: float64 >>> print(portfolio.shares) 2018-01-01 10.0 2018-01-02 10.0 2018-01-03 10.0 2018-01-04 10.0 2018-01-05 10.0 dtype: float64 >>> print(portfolio.equity) 2018-01-01 99.975 2018-01-02 109.975 2018-01-03 119.975 2018-01-04 109.975 2018-01-05 99.975 dtype: float64 >>> print(portfolio.total_costs) 0.02499999999999858 ``` """ # Get defaults if init_capital is None: init_capital = defaults.portfolio['init_capital'] init_capital = float(init_capital) if fees is None: fees = defaults.portfolio['fees'] if slippage is None: slippage = defaults.portfolio['slippage'] # Perform checks checks.assert_type(price, (pd.Series, pd.DataFrame)) checks.assert_numba_func(order_func_nb) # Broadcast inputs fees = reshape_fns.broadcast_to(fees, price, to_pd=False, writeable=True) slippage = reshape_fns.broadcast_to(slippage, price, to_pd=False, writeable=True) # Perform calculation cash, shares, paid_fees, paid_slippage = nb.portfolio_nb( reshape_fns.to_2d(price, raw=True), init_capital, reshape_fns.to_2d(fees, raw=True), reshape_fns.to_2d(slippage, raw=True), order_func_nb, *args) # Bring to the same meta cash = price.vbt.wrap_array(cash) shares = price.vbt.wrap_array(shares) paid_fees = price.vbt.wrap_array(paid_fees) paid_slippage = price.vbt.wrap_array(paid_slippage) return cls(price, cash, shares, init_capital, paid_fees, paid_slippage)
def from_orders(cls, price, orders, is_target=False, init_capital=None, fees=None, slippage=None, broadcast_kwargs={}): """Build portfolio from orders. Starting with initial capital `init_capital`, at each time step, orders the number of shares specified in `orders`. Args: price (pandas_like): Price of the asset. orders (int, float or array_like): The amount of shares to order. If the amount is positive, this is the number of shares to buy. If the amount is negative, this is the number of shares to sell. To buy/sell everything, set the amount to `numpy.inf`. is_target (bool): If `True`, will order the difference between current and target amount. init_capital (int or float): The initial capital. fees (float or array_like): Trading fees in percentage of the value involved. slippage (float or array_like): Slippage in percentage of `price`. All array-like arguments will be broadcasted together using `vectorbt.utils.reshape_fns.broadcast` with `broadcast_kwargs`. At the end, each time series object will have the same metadata. Example: Portfolio value of various order sequences: ```python-repl >>> orders = pd.DataFrame({ ... 'a': [np.inf, 0, 0, 0, 0], ... 'b': [1, 1, 1, 1, -np.inf], ... 'c': [np.inf, -np.inf, np.inf, -np.inf, np.inf] ... }, index=index) >>> portfolio = vbt.Portfolio.from_orders(price, orders, ... init_capital=100, fees=0.0025) >>> print(portfolio.cash) a b c 2018-01-01 0.0 98.9975 0.000000 2018-01-02 0.0 96.9925 199.002494 2018-01-03 0.0 93.9850 0.000000 2018-01-04 0.0 91.9800 132.006642 2018-01-05 0.0 95.9700 0.000000 >>> print(portfolio.shares) a b c 2018-01-01 99.750623 1.0 99.750623 2018-01-02 99.750623 2.0 0.000000 2018-01-03 99.750623 3.0 66.168743 2018-01-04 99.750623 4.0 0.000000 2018-01-05 99.750623 0.0 131.677448 >>> print(portfolio.equity) a b c 2018-01-01 99.750623 99.9975 99.750623 2018-01-02 199.501247 100.9925 199.002494 2018-01-03 299.251870 102.9850 198.506228 2018-01-04 199.501247 99.9800 132.006642 2018-01-05 99.750623 95.9700 131.677448 >>> print(portfolio.total_costs) a 0.249377 b 0.030000 c 1.904433 dtype: float64 ``` """ # Get defaults if init_capital is None: init_capital = defaults.portfolio['init_capital'] init_capital = float(init_capital) if fees is None: fees = defaults.portfolio['fees'] if slippage is None: slippage = defaults.portfolio['slippage'] # Perform checks checks.assert_type(price, (pd.Series, pd.DataFrame)) checks.assert_type(orders, (pd.Series, pd.DataFrame)) # Broadcast inputs price, orders = reshape_fns.broadcast(price, orders, **broadcast_kwargs, writeable=True) fees = reshape_fns.broadcast_to(fees, price, to_pd=False, writeable=True) slippage = reshape_fns.broadcast_to(slippage, price, to_pd=False, writeable=True) # Perform calculation cash, shares, paid_fees, paid_slippage = nb.portfolio_nb( reshape_fns.to_2d(price, raw=True), init_capital, reshape_fns.to_2d(fees, raw=True), reshape_fns.to_2d(slippage, raw=True), nb.amount_order_func_nb, reshape_fns.to_2d(orders, raw=True), is_target) # Bring to the same meta cash = price.vbt.wrap_array(cash) shares = price.vbt.wrap_array(shares) paid_fees = price.vbt.wrap_array(paid_fees) paid_slippage = price.vbt.wrap_array(paid_slippage) return cls(price, cash, shares, init_capital, paid_fees, paid_slippage)
def from_signals(cls, price, entries, exits, amount=np.inf, init_capital=None, fees=None, slippage=None, broadcast_kwargs={}): """Build portfolio from entry and exit signals. Starting with initial capital `init_capital`, for each `True` in `entries`/`exits`, orders the number of shares specified in `amount`. Args: price (pandas_like): Price of the asset. entries (pandas_like): Boolean array of entry signals. exits (pandas_like): Boolean array of exit signals. amount (int, float or array_like): The amount of shares to order. To buy/sell everything, set the amount to `numpy.inf`. init_capital (int or float): The initial capital. fees (float or array_like): Trading fees in percentage of the value involved. slippage (float or array_like): Slippage in percentage of `price`. All array-like arguments will be broadcasted together using `vectorbt.utils.reshape_fns.broadcast` with `broadcast_kwargs`. At the end, each time series object will have the same metadata. !!! note There is no mechanism implemented to prevent order accumulation, meaning multiple entry/exit signals one after another may increase/decrease your position in the market. That's why we will later calculate P/L of positions instead of trades. To select at most one exit signal, use `vectorbt.signals.accessors.Signals_Accessor.first`. Example: Portfolio value of various signal sequences: ```python-repl >>> entries = pd.DataFrame({ ... 'a': [True, False, False, False, False], ... 'b': [True, True, True, True, True], ... 'c': [True, False, True, False, True] ... }, index=index) >>> exits = pd.DataFrame({ ... 'a': [False, False, False, False, False], ... 'b': [False, False, False, False, False], ... 'c': [False, True, False, True, False] ... }, index=index) >>> portfolio = vbt.Portfolio.from_signals(price, entries, ... exits, amount=10, init_capital=100, fees=0.0025) >>> print(portfolio.cash) a b c 2018-01-01 89.975 89.975 89.975 2018-01-02 89.975 69.925 109.925 2018-01-03 89.975 39.850 79.850 2018-01-04 89.975 19.800 99.800 2018-01-05 89.975 9.775 89.775 >>> print(portfolio.shares) a b c 2018-01-01 10.0 10.0 10.0 2018-01-02 10.0 20.0 0.0 2018-01-03 10.0 30.0 10.0 2018-01-04 10.0 40.0 0.0 2018-01-05 10.0 50.0 10.0 >>> print(portfolio.equity) a b c 2018-01-01 99.975 99.975 99.975 2018-01-02 109.975 109.925 109.925 2018-01-03 119.975 129.850 109.850 2018-01-04 109.975 99.800 99.800 2018-01-05 99.975 59.775 99.775 >>> print(portfolio.total_costs) a 0.025 b 0.225 c 0.225 dtype: float64 ``` """ # Get defaults if init_capital is None: init_capital = defaults.portfolio['init_capital'] init_capital = float(init_capital) if fees is None: fees = defaults.portfolio['fees'] if slippage is None: slippage = defaults.portfolio['slippage'] # Perform checks checks.assert_type(price, (pd.Series, pd.DataFrame)) checks.assert_type(entries, (pd.Series, pd.DataFrame)) checks.assert_type(exits, (pd.Series, pd.DataFrame)) entries.vbt.signals.validate() exits.vbt.signals.validate() # Broadcast inputs price, entries, exits, amount, fees, slippage = reshape_fns.broadcast( price, entries, exits, amount, fees, slippage, **broadcast_kwargs, writeable=True) # Perform calculation cash, shares, paid_fees, paid_slippage = nb.portfolio_nb( reshape_fns.to_2d(price, raw=True), init_capital, reshape_fns.to_2d(fees, raw=True), reshape_fns.to_2d(slippage, raw=True), nb.signals_order_func_nb, reshape_fns.to_2d(entries, raw=True), reshape_fns.to_2d(exits, raw=True), reshape_fns.to_2d(amount, raw=True)) # Bring to the same meta cash = price.vbt.wrap_array(cash) shares = price.vbt.wrap_array(shares) paid_fees = price.vbt.wrap_array(paid_fees) paid_slippage = price.vbt.wrap_array(paid_slippage) return cls(price, cash, shares, init_capital, paid_fees, paid_slippage)
def from_params_pipeline(ts_list, param_list, level_names, num_outputs, custom_func, *args, pass_lists=False, param_product=False, broadcast_kwargs={}, return_raw=False, **kwargs): """A pipeline for calculating an indicator, used by `IndicatorFactory`. Does the following: * Takes one or multiple time series objects in `ts_list` and broadcasts them. For example: ```python-repl >>> sr = pd.Series([1, 2], index=['x', 'y']) >>> df = pd.DataFrame([[3, 4], [5, 6]], index=['x', 'y'], columns=['a', 'b']) >>> ts_list = [sr, df] >>> ts_list = vbt.utils.reshape_fns.broadcast(*ts_list) >>> print(ts_list[0]) a b x 1 1 y 2 2 >>> print(ts_list[1]) a b x 3 4 y 5 6 ``` * Takes one or multiple parameters in `param_list`, converts them to NumPy arrays and broadcasts them. For example: ```python-repl >>> p1, p2, p3 = 1, [2, 3, 4], [False] >>> param_list = [p1, p2, p3] >>> param_list = vbt.utils.reshape_fns.broadcast(*param_list) >>> print(param_list[0]) array([1, 1, 1]) >>> print(param_list[1]) array([2, 3, 4]) >>> print(param_list[2]) array([False, False, False]) ``` * Performs calculation using `custom_func` to build output arrays (`output_list`) and other objects (`other_list`, optional). For example: ```python-repl >>> def custom_func(ts1, ts2, p1, p2, p3, *args, **kwargs): ... return pd.DataFrame.vbt.concat( ... (ts1.values + ts2.values) + p1[0] * p2[0], ... (ts1.values + ts2.values) + p1[1] * p2[1], ... (ts1.values + ts2.values) + p1[2] * p2[2] ... ) >>> output = custom_func(*ts_list, *param_list) >>> print(output) array([[ 6, 7, 7, 8, 8, 9], [ 9, 10, 10, 11, 11, 12]]) ``` * Creates new column hierarchy based on parameters and level names. For example: ```python-repl >>> p1_columns = pd.Index(param_list[0], name='p1') >>> p2_columns = pd.Index(param_list[1], name='p2') >>> p3_columns = pd.Index(param_list[2], name='p3') >>> p_columns = vbt.utils.index_fns.stack(p1_columns, p2_columns, p3_columns) >>> new_columns = vbt.utils.index_fns.combine(p_columns, ts_list[0].columns) >>> output_df = pd.DataFrame(output, columns=new_columns) >>> print(output_df) p1 1 1 1 1 1 1 p2 2 2 3 3 4 4 p3 False False False False False False a b a b a b 0 6 7 7 8 8 9 1 9 10 10 11 11 12 ``` * Broadcasts objects in `ts_list` to match the shape of objects in `output_list` through tiling. This is done to be able to compare them and generate signals, since you cannot compare NumPy arrays that have totally different shapes, such as (2, 2) and (2, 6). For example: ```python-repl >>> new_ts_list = [ ... ts_list[0].vbt.tile(len(param_list[0]), as_columns=p_columns), ... ts_list[1].vbt.tile(len(param_list[0]), as_columns=p_columns) ... ] >>> print(new_ts_list[0]) p1 1 1 1 1 1 1 p2 2 2 3 3 4 4 p3 False False False False False False a b a b a b 0 1 1 1 1 1 1 1 2 2 2 2 2 2 ``` * Builds parameter mappers that will link parameters from `param_list` to columns in `ts_list` and `output_list`. This is done to enable column indexing using parameter values. Args: ts_list (list of array_like): A list of time series objects. At least one must be a pandas object. param_list (list of array_like): A list of parameters. Each element is either an array-like object or a single value of any type. level_names (list of str): A list of column level names corresponding to each parameter. num_outputs (int): The number of output arrays. custom_func (function): A custom calculation function. See `IndicatorFactory.from_custom_func`. *args: Arguments passed to the `custom_func`. pass_lists (bool): If True, arguments are passed to the `custom_func` as lists. Defaults to False. param_product (bool): If True, builds a Cartesian product out of all parameters. Defaults to False. broadcast_kwargs (dict, optional): Keyword arguments passed to the `vectorbt.utils.reshape_fns.broadcast` on time series objects. return_raw (bool): If True, returns the raw output without post-processing. Defaults to False. **kwargs: Keyword arguments passed to the `custom_func`. Some common arguments include `return_cache` to return cache and `cache` to pass cache. Those are only applicable to `custom_func` that supports it (`custom_func` created using `IndicatorFactory.from_apply_func` are supported by default). Returns: A list of transformed inputs (`pandas_like`), a list of generated outputs (`pandas_like`), a list of parameter arrays (`numpy.ndarray`), a list of parameter mappers (`pandas.Series`), a list of other generated outputs that are outside of `num_outputs`. """ # Check time series objects checks.assert_type(ts_list[0], (pd.Series, pd.DataFrame)) for i in range(1, len(ts_list)): ts_list[i].vbt.timeseries.validate() if len(ts_list) > 1: # Broadcast time series ts_list = reshape_fns.broadcast(*ts_list, **broadcast_kwargs, writeable=True) # Check level names checks.assert_type(level_names, (list, tuple)) checks.assert_same_len(param_list, level_names) for ts in ts_list: # Every time series object should be free of the specified level names in its columns for level_name in level_names: checks.assert_level_not_exists(ts, level_name) # Convert params to 1-dim arrays param_list = list(map(reshape_fns.to_1d, param_list)) if len(param_list) > 1: if param_product: # Make Cartesian product out of all params param_list = list(map(reshape_fns.to_1d, param_list)) param_list = list(zip(*list(itertools.product(*param_list)))) param_list = list(map(np.asarray, param_list)) else: # Broadcast such that each array has the same length param_list = reshape_fns.broadcast(*param_list, writeable=True) # Perform main calculation if pass_lists: output_list = custom_func(ts_list, param_list, *args, **kwargs) else: output_list = custom_func(*ts_list, *param_list, *args, **kwargs) if return_raw or kwargs.get('return_cache', False): return output_list # return raw cache outputs if not isinstance(output_list, (tuple, list, List)): output_list = [output_list] else: output_list = list(output_list) # Other outputs should be returned without post-processing (for example cache_dict) if len(output_list) > num_outputs: other_list = output_list[num_outputs:] else: other_list = [] # Process only the num_outputs outputs output_list = output_list[:num_outputs] if len(param_list) > 0: # Build new column levels on top of time series levels new_columns = build_column_hierarchy( param_list, level_names, reshape_fns.to_2d(ts_list[0]).columns) # Wrap into new pandas objects both time series and output objects new_ts_list = list( map(lambda x: broadcast_ts(x, param_list[0].shape[0], new_columns), ts_list)) # Build mappers to easily map between parameters and columns mapper_list = [ build_mapper(x, ts_list[0], new_columns, level_names[i]) for i, x in enumerate(param_list) ] else: # Some indicators don't have any params new_columns = reshape_fns.to_2d(ts_list[0]).columns new_ts_list = list(ts_list) mapper_list = [] output_list = list( map(lambda x: wrap_output(x, ts_list[0], new_columns), output_list)) if len(mapper_list) > 1: # Tuple object is a mapper that accepts tuples of parameters tuple_mapper = build_tuple_mapper(mapper_list, new_columns, tuple(level_names)) mapper_list.append(tuple_mapper) return new_ts_list, output_list, param_list, mapper_list, other_list
def to_2d_array(self): """Convert to 2-dim NumPy array. See `vectorbt.utils.reshape_fns.to_2d`.""" return reshape_fns.to_2d(self._obj, raw=True)
def combine_with_multiple(self, others, *args, combine_func=None, pass_2d=False, concat=False, broadcast_kwargs={}, as_columns=None, **kwargs): """Combine with `others` using `combine_func`. All arguments will be broadcasted using `vectorbt.utils.reshape_fns.broadcast` with `broadcast_kwargs`. If `concat` is `True`, concatenate the results along columns, see `vectorbt.utils.combine_fns.combine_and_concat`. Otherwise, pairwise combine into a Series/DataFrame of the same shape, see `vectorbt.utils.combine_fns.combine_multiple`. Arguments `*args` and `**kwargs` will be directly passed to `combine_func`. If `pass_2d` is `True`, 2-dimensional NumPy arrays will be passed, otherwise as is. Use `as_columns` as a top-level column level. !!! note If `combine_func` is Numba-compiled, will broadcast using `writeable=True` and copy using `order='C'` flags, which can lead to an expensive computation overhead if passed objects are large and have different shape/memory order. You also must ensure that all objects have the same data type. Also remember to bring each in `*args` to a Numba-compatible format. Example: ```python-repl >>> import pandas as pd >>> sr = pd.Series([1, 2], index=['x', 'y']) >>> df = pd.DataFrame([[3, 4], [5, 6]], index=['x', 'y'], columns=['a', 'b']) >>> print(sr.vbt.combine_with_multiple([df, df*2], ... combine_func=lambda x, y: x + y)) a b x 10 13 y 17 20 >>> print(sr.vbt.combine_with_multiple([df, df*2], ... combine_func=lambda x, y: x + y, concat=True, as_columns=['c', 'd'])) c d a b a b x 4 5 7 9 y 7 8 12 14 ```""" others = tuple( map(lambda x: x._obj if isinstance(x, Base_Accessor) else x, others)) checks.assert_not_none(combine_func) checks.assert_type(others, Iterable) # Broadcast arguments if checks.is_numba_func(combine_func): # Numba requires writable arrays broadcast_kwargs = {**dict(writeable=True), **broadcast_kwargs} # Plus all of our arrays must be in the same order broadcast_kwargs['copy_kwargs'] = { **dict(order='C'), **broadcast_kwargs.get('copy_kwargs', {}) } new_obj, *new_others = reshape_fns.broadcast(self._obj, *others, **broadcast_kwargs) # Optionally cast to 2d array if pass_2d: bc_arrays = tuple( map(lambda x: reshape_fns.to_2d(np.asarray(x)), (new_obj, *new_others))) else: bc_arrays = tuple( map(lambda x: np.asarray(x), (new_obj, *new_others))) if concat: # Concat the results horizontally if checks.is_numba_func(combine_func): for i in range(1, len(bc_arrays)): checks.assert_same_meta(bc_arrays[i - 1], bc_arrays[i]) result = combine_fns.combine_and_concat_nb( bc_arrays[0], bc_arrays[1:], combine_func, *args, **kwargs) else: result = combine_fns.combine_and_concat( bc_arrays[0], bc_arrays[1:], combine_func, *args, **kwargs) columns = new_obj.vbt.columns if as_columns is not None: new_columns = index_fns.combine_indexes(as_columns, columns) else: new_columns = index_fns.tile_index(columns, len(others)) return new_obj.vbt.wrap_array(result, columns=new_columns) else: # Combine arguments pairwise into one object if checks.is_numba_func(combine_func): for i in range(1, len(bc_arrays)): checks.assert_same_dtype(bc_arrays[i - 1], bc_arrays[i]) result = combine_fns.combine_multiple_nb( bc_arrays, combine_func, *args, **kwargs) else: result = combine_fns.combine_multiple(bc_arrays, combine_func, *args, **kwargs) return new_obj.vbt.wrap_array(result)
def from_orders(cls, price, order_size, order_price=None, init_capital=None, fees=None, fixed_fees=None, slippage=None, is_target=False, broadcast_kwargs={}, **kwargs): """Build portfolio from orders. Starting with initial capital `init_capital`, at each time step, orders the number of shares specified in `order_size` for `order_price`. Args: price (pandas_like): Main price of the asset, such as close. order_size (int, float or array_like): The amount of shares to order. If the size is positive, this is the number of shares to buy. If the size is negative, this is the number of shares to sell. To buy/sell everything, set the size to `numpy.inf`. order_price (array_like): Order price. Defaults to `price`. init_capital (int or float): The initial capital. fees (float or array_like): Fees in percentage of the order value. fixed_fees (float or array_like): Fixed amount of fees to pay per order. slippage (float or array_like): Slippage in percentage of `order_price`. is_target (bool): If `True`, will order the difference between current and target size. **kwargs: Keyword arguments passed to the `__init__` method. For defaults, see `vectorbt.defaults.portfolio`. All array-like arguments will be broadcasted together using `vectorbt.utils.reshape_fns.broadcast` with `broadcast_kwargs`. At the end, all array objects will have the same metadata. Example: Portfolio from various order sequences: ```python-repl >>> orders = pd.DataFrame({ ... 'a': [np.inf, 0, 0, 0, 0], ... 'b': [1, 1, 1, 1, -np.inf], ... 'c': [np.inf, -np.inf, np.inf, -np.inf, np.inf] ... }, index=index) >>> portfolio = vbt.Portfolio.from_orders(price, orders, ... init_capital=100, fees=0.0025, fixed_fees=1., slippage=0.001) >>> print(portfolio.order_records) Column Index Size Price Fees Side 0 0.0 0.0 98.654463 1.001 1.246883 0.0 1 1.0 0.0 1.000000 1.001 1.002502 0.0 2 1.0 1.0 1.000000 2.002 1.005005 0.0 3 1.0 2.0 1.000000 3.003 1.007507 0.0 4 1.0 3.0 1.000000 2.002 1.005005 0.0 5 1.0 4.0 4.000000 0.999 1.009990 1.0 6 2.0 0.0 98.654463 1.001 1.246883 0.0 7 2.0 1.0 98.654463 1.998 1.492779 1.0 8 2.0 2.0 64.646521 3.003 1.485334 0.0 9 2.0 3.0 64.646521 1.998 1.322909 1.0 10 2.0 4.0 126.398131 1.001 1.316311 0.0 >>> print(portfolio.shares) a b c 2018-01-01 98.654463 1.0 98.654463 2018-01-02 98.654463 2.0 0.000000 2018-01-03 98.654463 3.0 64.646521 2018-01-04 98.654463 4.0 0.000000 2018-01-05 98.654463 0.0 126.398131 >>> print(portfolio.cash) a b c 2018-01-01 0.0 97.996498 0.000000e+00 2018-01-02 0.0 94.989493 1.956188e+02 2018-01-03 0.0 90.978985 2.842171e-14 2018-01-04 0.0 87.971980 1.278408e+02 2018-01-05 0.0 90.957990 0.000000e+00 ``` """ # Get defaults if order_price is None: order_price = price if init_capital is None: init_capital = defaults.portfolio['init_capital'] init_capital = float(init_capital) if fees is None: fees = defaults.portfolio['fees'] if fixed_fees is None: fixed_fees = defaults.portfolio['fixed_fees'] if slippage is None: slippage = defaults.portfolio['slippage'] # Perform checks checks.assert_type(price, (pd.Series, pd.DataFrame)) # Broadcast inputs price, order_size, order_price, fees, fixed_fees, slippage = \ reshape_fns.broadcast(price, order_size, order_price, fees, fixed_fees, slippage, **broadcast_kwargs, writeable=True) # Perform calculation order_records, cash, shares = nb.simulate_from_orders_nb( reshape_fns.to_2d(price, raw=True).shape, init_capital, reshape_fns.to_2d(order_size, raw=True), reshape_fns.to_2d(order_price, raw=True), reshape_fns.to_2d(fees, raw=True), reshape_fns.to_2d(fixed_fees, raw=True), reshape_fns.to_2d(slippage, raw=True), is_target) # Bring to the same meta cash = price.vbt.wrap(cash) shares = price.vbt.wrap(shares) return cls(price, init_capital, order_records, cash, shares, **kwargs)
def from_order_func(cls, price, order_func_nb, *args, init_capital=None, **kwargs): """Build portfolio from a custom order function. Starting with initial capital `init_capital`, iterates over shape `price.shape`, and for each data point, generates an order using `order_func_nb`. This way, you can specify order size, price and transaction costs dynamically (for example, based on the current balance). To iterate over a bigger shape than `price`, you should tile/repeat `price` to the desired shape. Args: price (pandas_like): Main price of the asset, such as close. Must be a pandas object. order_func_nb (function): Function that returns an order. See `vectorbt.portfolio.enums.Order`. *args: Arguments passed to `order_func_nb`. init_capital (int or float): The initial capital. **kwargs: Keyword arguments passed to the `__init__` method. For defaults, see `vectorbt.defaults.portfolio`. !!! note `order_func_nb` must be Numba-compiled. Example: Portfolio from buying daily: ```python-repl >>> from vectorbt.portfolio.nb import Order >>> @njit ... def order_func_nb(col, i, run_cash, run_shares, price): ... return Order(10, price[i], fees=0.01, fixed_fees=1., slippage=0.01) >>> portfolio = vbt.Portfolio.from_order_func( ... price, order_func_nb, price.values, init_capital=100) >>> print(portfolio.order_records) Column Index Size Price Fees Side 0 0.0 0.0 10.0 1.01 1.101 0.0 1 0.0 1.0 10.0 2.02 1.202 0.0 2 0.0 2.0 10.0 3.03 1.303 0.0 3 0.0 3.0 10.0 2.02 1.202 0.0 4 0.0 4.0 10.0 1.01 1.101 0.0 >>> print(portfolio.shares) 2018-01-01 10.0 2018-01-02 20.0 2018-01-03 30.0 2018-01-04 40.0 2018-01-05 50.0 Name: a, dtype: float64 >>> print(portfolio.cash) 2018-01-01 88.799 2018-01-02 67.397 2018-01-03 35.794 2018-01-04 14.392 2018-01-05 3.191 Name: a, dtype: float64 ``` """ # Get defaults if init_capital is None: init_capital = defaults.portfolio['init_capital'] init_capital = float(init_capital) # Perform checks checks.assert_type(price, (pd.Series, pd.DataFrame)) checks.assert_numba_func(order_func_nb) # Perform calculation order_records, cash, shares = nb.simulate_nb( reshape_fns.to_2d(price, raw=True).shape, init_capital, order_func_nb, *args) # Bring to the same meta cash = price.vbt.wrap(cash) shares = price.vbt.wrap(shares) return cls(price, init_capital, order_records, cash, shares, **kwargs)
def combine_with_multiple(self, others, *args, combine_func=None, concat=False, broadcast_kwargs={}, as_columns=None, **kwargs): """Broadcast with other objects to the same shape and combine them all pairwise. The returned shape is the same as broadcasted shape if concat is False. The returned shape is concatenation of broadcasted shapes if concat is True.""" others = tuple( map(lambda x: x._obj if isinstance(x, Base_Accessor) else x, others)) checks.assert_not_none(combine_func) checks.assert_type(others, Iterable) # Broadcast arguments if checks.is_numba_func(combine_func): # Numba requires writable arrays broadcast_kwargs = {**dict(writeable=True), **broadcast_kwargs} # Plus all of our arrays must be in the same order broadcast_kwargs['copy_kwargs'] = { **dict(order='C'), **broadcast_kwargs.get('copy_kwargs', {}) } new_obj, *new_others = reshape_fns.broadcast(self._obj, *others, **broadcast_kwargs) broadcasted = tuple(map(np.asarray, (new_obj, *new_others))) if concat: # Concat the results horizontally if checks.is_numba_func(combine_func): for i in range(1, len(broadcasted)): # NOTE: all inputs must have the same dtype checks.assert_same_meta(broadcasted[i - 1], broadcasted[i]) result = combine_fns.combine_and_concat_nb( broadcasted[0], broadcasted[1:], combine_func, *args, **kwargs) else: result = combine_fns.combine_and_concat( broadcasted[0], broadcasted[1:], combine_func, *args, **kwargs) if as_columns is not None: new_columns = index_fns.combine( as_columns, reshape_fns.to_2d(new_obj).columns) else: new_columns = index_fns.tile( reshape_fns.to_2d(new_obj).columns, len(others)) return new_obj.vbt.wrap_array(result, columns=new_columns) else: # Combine arguments pairwise into one object if checks.is_numba_func(combine_func): for i in range(1, len(broadcasted)): # NOTE: all inputs must have the same dtype checks.assert_same_dtype(broadcasted[i - 1], broadcasted[i]) result = combine_fns.combine_multiple_nb( broadcasted, combine_func, *args, **kwargs) else: result = combine_fns.combine_multiple(broadcasted, combine_func, *args, **kwargs) return new_obj.vbt.wrap_array(result)
def to_2d_array(self): return reshape_fns.to_2d(self._obj, raw=True)