def __init__(self, obj, freq=None): if not checks.is_pandas(obj): # parent accessor obj = obj._obj self._obj = obj # Initialize array wrapper wrapper = ArrayWrapper.from_obj(obj) ArrayWrapper.__init__(self, index=wrapper.index, columns=wrapper.columns, ndim=wrapper.ndim, freq=freq)
def _mapped_indexing_func(obj, pd_indexing_func): """Perform indexing on `MappedArray`.""" if obj.wrapper.ndim == 1: raise TypeError("Indexing on Series is not supported") n_rows = len(obj.wrapper.index) n_cols = len(obj.wrapper.columns) col_mapper = obj.wrapper.wrap( np.broadcast_to(np.arange(n_cols), (n_rows, n_cols))) col_mapper = pd_indexing_func(col_mapper) if not pd.Index.equals(col_mapper.index, obj.wrapper.index): raise NotImplementedError( "Changing index (time axis) is not supported") new_cols = reshape_fns.to_1d(col_mapper.values[0]) # array required new_indices, new_col_arr = nb.select_mapped_cols_nb( obj.col_arr, obj.col_index, new_cols) new_mapped_arr = obj.mapped_arr[new_indices] if obj.idx_arr is not None: new_idx_arr = obj.idx_arr[new_indices] else: new_idx_arr = None new_wrapper = ArrayWrapper.from_obj(col_mapper, freq=obj.wrapper.freq) return obj.__class__(new_mapped_arr, new_col_arr, new_wrapper, idx_arr=new_idx_arr)
def __init__(self, wrapper: ArrayWrapper, records_arr: tp.RecordArray, close: tp.ArrayLike, idx_field: str = 'exit_idx', trade_type: int = TradeType.Trade, **kwargs) -> None: Records.__init__(self, wrapper, records_arr, idx_field=idx_field, close=close, trade_type=trade_type, **kwargs) self._close = broadcast_to(close, wrapper.dummy(group_by=False)) self._trade_type = trade_type if trade_type == TradeType.Trade: if not all(field in records_arr.dtype.names for field in trade_dt.names): raise TypeError("Records array must match trade_dt") else: if not all(field in records_arr.dtype.names for field in position_dt.names): raise TypeError("Records array must match position_dt")
def from_ts(cls, ts, idx_field='end_idx', **kwargs): """Build `Drawdowns` from time series `ts`. `**kwargs` such as `freq` will be passed to `Drawdowns.__init__`.""" records_arr = nb.find_drawdowns_nb(ts.vbt.to_2d_array()) wrapper = ArrayWrapper.from_obj(ts, **kwargs) return cls(wrapper, records_arr, ts, idx_field=idx_field)
def from_ts(cls: tp.Type[RangesT], ts: tp.ArrayLike, gap_value: tp.Optional[tp.Scalar] = None, attach_ts: bool = True, wrapper_kwargs: tp.KwargsLike = None, **kwargs) -> RangesT: """Build `Ranges` from time series `ts`. Searches for sequences of * True values in boolean data (False acts as a gap), * positive values in integer data (-1 acts as a gap), and * non-NaN values in any other data (NaN acts as a gap). `**kwargs` will be passed to `Ranges.__init__`.""" if wrapper_kwargs is None: wrapper_kwargs = {} ts_pd = to_pd_array(ts) ts_arr = to_2d_array(ts_pd) if gap_value is None: if np.issubdtype(ts_arr.dtype, np.bool_): gap_value = False elif np.issubdtype(ts_arr.dtype, np.integer): gap_value = -1 else: gap_value = np.nan records_arr = nb.find_ranges_nb(ts_arr, gap_value) wrapper = ArrayWrapper.from_obj(ts_pd, **wrapper_kwargs) return cls(wrapper, records_arr, ts=ts_pd if attach_ts else None, **kwargs)
def clean(self_or_cls, *args, entry_first=True, broadcast_kwargs=None, wrap_kwargs=None): """Clean signals. If one array passed, see `SignalsAccessor.first`. If two arrays passed, entries and exits, see `vectorbt.signals.nb.clean_enex_nb`.""" if not isinstance(self_or_cls, type): args = (self_or_cls._obj, *args) if len(args) == 1: obj = args[0] if not isinstance(obj, (pd.Series, pd.DataFrame)): wrapper = ArrayWrapper.from_shape(np.asarray(obj).shape) obj = wrapper.wrap(obj, **merge_dicts({}, wrap_kwargs)) return obj.vbt.signals.first() elif len(args) == 2: if broadcast_kwargs is None: broadcast_kwargs = {} entries, exits = reshape_fns.broadcast(*args, **broadcast_kwargs) entries_out, exits_out = nb.clean_enex_nb( entries.vbt.to_2d_array(), exits.vbt.to_2d_array(), entry_first ) return ( entries.vbt.wrapper.wrap(entries_out, **merge_dicts({}, wrap_kwargs)), exits.vbt.wrapper.wrap(exits_out, **merge_dicts({}, wrap_kwargs)) ) else: raise ValueError("Either one or two arrays must be passed")
def repeat(self, n: int, keys: tp.Optional[tp.IndexLike] = None, axis: int = 1, wrap_kwargs: tp.KwargsLike = None) -> tp.SeriesFrame: """See `vectorbt.base.reshape_fns.repeat`. Set `axis` to 1 for columns and 0 for index. Use `keys` as the outermost level.""" repeated = reshape_fns.repeat(self.obj, n, axis=axis) if keys is not None: if axis == 1: new_columns = index_fns.combine_indexes([self.wrapper.columns, keys]) return ArrayWrapper.from_obj(repeated).wrap( repeated.values, **merge_dicts(dict(columns=new_columns), wrap_kwargs)) else: new_index = index_fns.combine_indexes([self.wrapper.index, keys]) return ArrayWrapper.from_obj(repeated).wrap( repeated.values, **merge_dicts(dict(index=new_index), wrap_kwargs)) return repeated
def from_ts(cls: tp.Type[DrawdownsT], ts: tp.ArrayLike, idx_field: str = 'end_idx', **kwargs) -> DrawdownsT: """Build `Drawdowns` from time series `ts`. `**kwargs` such as `freq` will be passed to `Drawdowns.__init__`.""" pd_ts = to_pd_array(ts) records_arr = nb.find_drawdowns_nb(pd_ts.vbt.to_2d_array()) wrapper = ArrayWrapper.from_obj(pd_ts, **kwargs) return cls(wrapper, records_arr, pd_ts, idx_field=idx_field)
def __init__(self, main_price, init_capital, orders, cash, shares, freq=None, year_freq=None, levy_alpha=None, risk_free=None, required_return=None, cutoff=None, factor_returns=None, incl_unrealized_stats=False): # Perform checks checks.assert_type(main_price, (pd.Series, pd.DataFrame)) if checks.is_frame(main_price): checks.assert_type(init_capital, pd.Series) checks.assert_same(main_price.columns, init_capital.index) else: checks.assert_ndim(init_capital, 0) checks.assert_same_meta(main_price, cash) checks.assert_same_meta(main_price, shares) # Store passed arguments self._main_price = main_price self._init_capital = init_capital self._orders = orders self._cash = cash self._shares = shares self._incl_unrealized_stats = incl_unrealized_stats freq = main_price.vbt(freq=freq).freq if freq is None: raise ValueError( "Couldn't parse the frequency of index. You must set `freq`.") self._freq = freq year_freq = main_price.vbt.returns(year_freq=year_freq).year_freq if freq is None: raise ValueError("You must set `year_freq`.") self._year_freq = year_freq # Parameters self._levy_alpha = defaults.portfolio[ 'levy_alpha'] if levy_alpha is None else levy_alpha self._risk_free = defaults.portfolio[ 'risk_free'] if risk_free is None else risk_free self._required_return = defaults.portfolio[ 'required_return'] if required_return is None else required_return self._cutoff = defaults.portfolio[ 'cutoff'] if cutoff is None else cutoff self._factor_returns = defaults.portfolio[ 'factor_returns'] if factor_returns is None else factor_returns # Supercharge PandasIndexer.__init__(self, _indexing_func) self.wrapper = ArrayWrapper.from_obj(main_price, freq=freq)
def __init__(self, records_arr, ts, freq=None, idx_field='end_idx'): Records.__init__(self, records_arr, ArrayWrapper.from_obj(ts, freq=freq), idx_field=idx_field) PandasIndexer.__init__(self, _indexing_func) if not all(field in records_arr.dtype.names for field in drawdown_dt.names): raise ValueError( "Records array must have all fields defined in drawdown_dt") self.ts = ts
def __init__(self, records_arr, main_price, freq=None, idx_field='idx'): Records.__init__(self, records_arr, ArrayWrapper.from_obj(main_price, freq=freq), idx_field=idx_field) PandasIndexer.__init__(self, _indexing_func) if not all(field in records_arr.dtype.names for field in order_dt.names): raise ValueError( "Records array must have all fields defined in order_dt") self.main_price = main_price
def from_ts(cls: tp.Type[DrawdownsT], ts: tp.ArrayLike, attach_ts: bool = True, wrapper_kwargs: tp.KwargsLike = None, **kwargs) -> DrawdownsT: """Build `Drawdowns` from time series `ts`. `**kwargs` will be passed to `Drawdowns.__init__`.""" ts_pd = to_pd_array(ts) records_arr = nb.get_drawdowns_nb(to_2d_array(ts_pd)) wrapper = ArrayWrapper.from_obj(ts_pd, **merge_dicts({}, wrapper_kwargs)) return cls(wrapper, records_arr, ts=ts_pd if attach_ts else None, **kwargs)
def __init__(self, obj: tp.SeriesFrame, wrapper: tp.Optional[ArrayWrapper] = None, **kwargs) -> None: checks.assert_instance_of(obj, (pd.Series, pd.DataFrame)) self._obj = obj wrapper_arg_names = get_func_arg_names(ArrayWrapper.__init__) grouper_arg_names = get_func_arg_names(ColumnGrouper.__init__) wrapping_kwargs = dict() for k in list(kwargs.keys()): if k in wrapper_arg_names or k in grouper_arg_names: wrapping_kwargs[k] = kwargs.pop(k) if wrapper is None: wrapper = ArrayWrapper.from_obj(obj, **wrapping_kwargs) else: wrapper = wrapper.replace(**wrapping_kwargs) Wrapping.__init__(self, wrapper, obj=obj, **kwargs)
def __init__(self, wrapper: ArrayWrapper, records_arr: tp.RecordArray, ts: tp.ArrayLike, idx_field: str = 'end_idx', **kwargs) -> None: Records.__init__(self, wrapper, records_arr, idx_field=idx_field, ts=ts, **kwargs) self._ts = broadcast_to(ts, wrapper.dummy(group_by=False)) if not all(field in records_arr.dtype.names for field in drawdown_dt.names): raise TypeError("Records array must match drawdown_dt")
def __init__(self, wrapper: ArrayWrapper, records_arr: tp.RecordArray, close: tp.ArrayLike, idx_field: str = 'idx', **kwargs) -> None: Records.__init__(self, wrapper, records_arr, idx_field=idx_field, close=close, **kwargs) self._close = broadcast_to(close, wrapper.dummy(group_by=False)) if not all(field in records_arr.dtype.names for field in order_dt.names): raise TypeError("Records array must match order_dt")
def indexing_on_records(obj, pd_indexing_func): """Perform indexing on `Records`.""" if obj.wrapper.ndim == 1: raise TypeError("Indexing on Series is not supported") n_rows = len(obj.wrapper.index) n_cols = len(obj.wrapper.columns) col_mapper = obj.wrapper.wrap( np.broadcast_to(np.arange(n_cols), (n_rows, n_cols))) col_mapper = pd_indexing_func(col_mapper) if not pd.Index.equals(col_mapper.index, obj.wrapper.index): raise NotImplementedError( "Changing index (time axis) is not supported") new_cols = reshape_fns.to_1d(col_mapper.values[0]) # array required records = nb.select_record_cols_nb(obj.records_arr, obj.col_index, new_cols) wrapper = ArrayWrapper.from_obj(col_mapper, freq=obj.wrapper.freq) return records, wrapper
def __init__(self, ts_list, output_list, param_list, mapper_list, name): perform_init_checks(ts_list, output_list, param_list, mapper_list, name) for i, ts_name in enumerate(ts_names): setattr(self, f'_{ts_name}', ts_list[i]) self.wrapper = ArrayWrapper.from_obj(ts_list[0]) for i, output_name in enumerate(output_names): setattr(self, f'_{output_name}', output_list[i]) for i, param_name in enumerate(param_names): setattr(self, f'_{param_name}_array', param_list[i]) setattr(self, f'_{param_name}_mapper', mapper_list[i]) if len(param_names) > 1: setattr(self, '_tuple_mapper', mapper_list[-1]) setattr(self, '_name', name) # Initialize indexers PandasIndexer.__init__(self, indexing_func) ParamIndexer.__init__(self, mapper_list, indexing_func)
def from_signals(cls, main_price, entries, exits, size=np.inf, entry_price=None, exit_price=None, init_capital=None, fees=None, fixed_fees=None, slippage=None, accumulate=False, broadcast_kwargs={}, freq=None, **kwargs): """Build portfolio from entry and exit signals. At each entry signal in `entries`, buys `size` of shares for `entry_price` to enter a position. At each exit signal in `exits`, sells everything for `exit_price` to exit the position. Accumulation of orders is disabled by default. Args: main_price (pandas_like): Main price of the asset, such as close. entries (array_like): Boolean array of entry signals. exits (array_like): Boolean array of exit signals. size (int, float or array_like): The amount of shares to order. To buy/sell everything, set the size to `np.inf`. entry_price (array_like): Entry price. Defaults to `main_price`. exit_price (array_like): Exit price. Defaults to `main_price`. init_capital (int, float or array_like): The initial capital. Single value or value per column. fees (float or array_like): Fees in percentage of the order value. Single value, value per column, or value per element. fixed_fees (float or array_like): Fixed amount of fees to pay per order. Single value, value per column, or value per element. slippage (float or array_like): Slippage in percentage of price. Single value, value per column, or value per element. accumulate (bool): If `accumulate` is `True`, entering the market when already in the market will be allowed to increase a position. broadcast_kwargs: Keyword arguments passed to `vectorbt.base.reshape_fns.broadcast`. freq (any): Index frequency in case `main_price.index` is not datetime-like. **kwargs: Keyword arguments passed to the `__init__` method. For defaults, see `vectorbt.defaults.portfolio`. All time series will be broadcasted together using `vectorbt.base.reshape_fns.broadcast`. At the end, they will have the same metadata. Example: Portfolio from various signal sequences: ```python-repl >>> entries = pd.DataFrame({ ... 'a': [True, False, False, False, False], ... 'b': [True, False, True, False, True], ... 'c': [True, True, True, True, True] ... }, index=index) >>> exits = pd.DataFrame({ ... 'a': [False, False, False, False, False], ... 'b': [False, True, False, True, False], ... 'c': [True, True, True, True, True] ... }, index=index) >>> portfolio = vbt.Portfolio.from_signals( ... price, entries, exits, size=10, ... init_capital=100, fees=0.0025, fixed_fees=1., slippage=0.001) >>> print(portfolio.orders.records) col idx size price fees side 0 0 0 10.0 1.001 1.025025 0 1 1 0 10.0 1.001 1.025025 0 2 1 1 10.0 1.998 1.049950 1 3 1 2 10.0 3.003 1.075075 0 4 1 3 10.0 1.998 1.049950 1 5 1 4 10.0 1.001 1.025025 0 6 2 0 10.0 1.001 1.025025 0 >>> print(portfolio.equity) a b c 2020-01-01 98.964975 98.964975 98.964975 2020-01-02 108.964975 107.895025 108.964975 2020-01-03 118.964975 106.789950 118.964975 2020-01-04 108.964975 95.720000 108.964975 2020-01-05 98.964975 94.684975 98.964975 ``` """ # Get defaults if entry_price is None: entry_price = main_price if exit_price is None: exit_price = main_price if init_capital is None: init_capital = defaults.portfolio['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(main_price, (pd.Series, pd.DataFrame)) checks.assert_dtype(entries, np.bool_) checks.assert_dtype(exits, np.bool_) # Broadcast inputs main_price, entries, exits, size, entry_price, exit_price, fees, fixed_fees, slippage = \ reshape_fns.broadcast( main_price, entries, exits, size, entry_price, exit_price, fees, fixed_fees, slippage, **broadcast_kwargs, writeable=True) target_shape = (main_price.shape[0], main_price.shape[1] if main_price.ndim > 1 else 1) init_capital = np.broadcast_to(init_capital, (target_shape[1],)) # Perform calculation order_records, cash, shares = nb.simulate_from_signals_nb( target_shape, init_capital, reshape_fns.to_2d(entries, raw=True), reshape_fns.to_2d(exits, raw=True), reshape_fns.to_2d(size, raw=True), reshape_fns.to_2d(entry_price, raw=True), reshape_fns.to_2d(exit_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), accumulate) # Bring to the same meta wrapper = ArrayWrapper.from_obj(main_price, freq=freq) cash = wrapper.wrap(cash) shares = wrapper.wrap(shares) orders = Orders(order_records, main_price, freq=freq) if checks.is_series(main_price): init_capital = init_capital[0] else: init_capital = wrapper.wrap_reduced(init_capital) return cls(main_price, init_capital, orders, cash, shares, freq=freq, **kwargs)
def combine(self, other: tp.MaybeTupleList[tp.Union[tp.ArrayLike, "BaseAccessor"]], *args, allow_multiple: bool = True, combine_func: tp.Optional[tp.Callable] = None, keep_pd: bool = False, to_2d: bool = False, concat: bool = False, numba_loop: bool = False, use_ray: bool = False, broadcast: bool = True, broadcast_kwargs: tp.KwargsLike = None, keys: tp.Optional[tp.IndexLike] = None, wrap_kwargs: tp.KwargsLike = None, **kwargs) -> tp.SeriesFrame: """Combine with `other` using `combine_func`. Args: other (array_like): Object to combine this array with. *args: Variable arguments passed to `combine_func`. allow_multiple (bool): Whether a tuple/list will be considered as multiple objects in `other`. combine_func (callable): Function to combine two arrays. Can be Numba-compiled. keep_pd (bool): Whether to keep inputs as pandas objects, otherwise convert to NumPy arrays. to_2d (bool): Whether to reshape inputs to 2-dim arrays, otherwise keep as-is. concat (bool): Whether to concatenate the results along the column axis. Otherwise, pairwise combine into a Series/DataFrame of the same shape. If True, see `vectorbt.base.combine_fns.combine_and_concat`. If False, see `vectorbt.base.combine_fns.combine_multiple`. numba_loop (bool): Whether to loop using Numba. Set to True when iterating large number of times over small input, but note that Numba doesn't support variable keyword arguments. use_ray (bool): Whether to use Ray to execute `combine_func` in parallel. Only works with `numba_loop` set to False and `concat` is set to True. See `vectorbt.base.combine_fns.ray_apply` for related keyword arguments. broadcast (bool): Whether to broadcast all inputs. broadcast_kwargs (dict): Keyword arguments passed to `vectorbt.base.reshape_fns.broadcast`. keys (index_like): Outermost column level. wrap_kwargs (dict): Keyword arguments passed to `vectorbt.base.array_wrapper.ArrayWrapper.wrap`. **kwargs: Keyword arguments passed to `combine_func`. !!! note If `combine_func` is Numba-compiled, will broadcast using `WRITEABLE` and `C_CONTIGUOUS` 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 vectorbt as vbt >>> 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']) >>> sr.vbt.combine(df, combine_func=lambda x, y: x + y) a b x 4 5 y 7 8 >>> sr.vbt.combine([df, df*2], combine_func=lambda x, y: x + y) a b x 10 13 y 17 20 >>> sr.vbt.combine([df, df*2], combine_func=lambda x, y: x + y, concat=True, keys=['c', 'd']) c d a b a b x 4 5 7 9 y 7 8 12 14 ``` Use Ray for small inputs and large processing times: ```python-repl >>> def combine_func(a, b): ... time.sleep(1) ... return a + b >>> sr = pd.Series([1, 2, 3]) >>> %timeit sr.vbt.combine([1, 1, 1], combine_func=combine_func) 3.01 s ± 2.98 ms per loop (mean ± std. dev. of 7 runs, 1 loop each) >>> %timeit sr.vbt.combine([1, 1, 1], combine_func=combine_func, concat=True, use_ray=True) 1.02 s ± 2.32 ms per loop (mean ± std. dev. of 7 runs, 1 loop each) ``` """ if not allow_multiple or not isinstance(other, (tuple, list)): others = (other, ) else: others = other others = tuple( map(lambda x: x.obj if isinstance(x, BaseAccessor) else x, others)) checks.assert_not_none(combine_func) # Broadcast arguments if broadcast: if broadcast_kwargs is None: broadcast_kwargs = {} if checks.is_numba_func(combine_func): # Numba requires writeable arrays # Plus all of our arrays must be in the same order broadcast_kwargs = merge_dicts( dict(require_kwargs=dict(requirements=['W', 'C'])), broadcast_kwargs) new_obj, *new_others = reshape_fns.broadcast( self.obj, *others, **broadcast_kwargs) else: new_obj, new_others = self.obj, others if not checks.is_pandas(new_obj): new_obj = ArrayWrapper.from_shape(new_obj.shape).wrap(new_obj) # Optionally cast to 2d array if to_2d: inputs = tuple( map(lambda x: reshape_fns.to_2d(x, raw=not keep_pd), (new_obj, *new_others))) else: if not keep_pd: inputs = tuple( map(lambda x: np.asarray(x), (new_obj, *new_others))) else: inputs = new_obj, *new_others if len(inputs) == 2: result = combine_func(inputs[0], inputs[1], *args, **kwargs) return ArrayWrapper.from_obj(new_obj).wrap( result, **merge_dicts({}, wrap_kwargs)) if concat: # Concat the results horizontally if checks.is_numba_func(combine_func) and numba_loop: if use_ray: raise ValueError("Ray cannot be used within Numba") for i in range(1, len(inputs)): checks.assert_meta_equal(inputs[i - 1], inputs[i]) result = combine_fns.combine_and_concat_nb( inputs[0], inputs[1:], combine_func, *args, **kwargs) else: if use_ray: result = combine_fns.combine_and_concat_ray( inputs[0], inputs[1:], combine_func, *args, **kwargs) else: result = combine_fns.combine_and_concat( inputs[0], inputs[1:], combine_func, *args, **kwargs) columns = ArrayWrapper.from_obj(new_obj).columns if keys is not None: new_columns = index_fns.combine_indexes([keys, columns]) else: top_columns = pd.Index(np.arange(len(new_others)), name='combine_idx') new_columns = index_fns.combine_indexes([top_columns, columns]) return ArrayWrapper.from_obj(new_obj).wrap( result, **merge_dicts(dict(columns=new_columns), wrap_kwargs)) else: # Combine arguments pairwise into one object if use_ray: raise ValueError("Ray cannot be used with concat=False") if checks.is_numba_func(combine_func) and numba_loop: for i in range(1, len(inputs)): checks.assert_dtype_equal(inputs[i - 1], inputs[i]) result = combine_fns.combine_multiple_nb( inputs, combine_func, *args, **kwargs) else: result = combine_fns.combine_multiple(inputs, combine_func, *args, **kwargs) return ArrayWrapper.from_obj(new_obj).wrap( result, **merge_dicts({}, wrap_kwargs))
def from_orders(cls, main_price, order_size, order_price=None, init_capital=None, fees=None, fixed_fees=None, slippage=None, is_target=False, broadcast_kwargs={}, freq=None, **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: main_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 `np.inf`. order_price (array_like): Order price. Defaults to `main_price`. init_capital (int, float or array_like): The initial capital. Single value or value per column. fees (float or array_like): Fees in percentage of the order value. Single value, value per column, or value per element. fixed_fees (float or array_like): Fixed amount of fees to pay per order. Single value, value per column, or value per element. slippage (float or array_like): Slippage in percentage of price. Single value, value per column, or value per element. is_target (bool): If `True`, will order the difference between current and target size. broadcast_kwargs: Keyword arguments passed to `vectorbt.base.reshape_fns.broadcast`. freq (any): Index frequency in case `main_price.index` is not datetime-like. **kwargs: Keyword arguments passed to the `__init__` method. For defaults, see `vectorbt.defaults.portfolio`. All time series will be broadcasted together using `vectorbt.base.reshape_fns.broadcast`. At the end, they will have the same metadata. Example: Portfolio from various order sequences: ```python-repl >>> portfolio = vbt.Portfolio.from_orders(price, orders, ... init_capital=100, fees=0.0025, fixed_fees=1., slippage=0.001) >>> print(portfolio.orders.records) col idx size price fees side 0 0 0 98.654463 1.001 1.246883 0 1 1 0 1.000000 1.001 1.002502 0 2 1 1 1.000000 2.002 1.005005 0 3 1 2 1.000000 3.003 1.007507 0 4 1 3 1.000000 2.002 1.005005 0 5 1 4 4.000000 0.999 1.009990 1 6 2 0 98.654463 1.001 1.246883 0 7 2 1 98.654463 1.998 1.492779 1 8 2 2 64.646521 3.003 1.485334 0 9 2 3 64.646521 1.998 1.322909 1 10 2 4 126.398131 1.001 1.316311 0 >>> print(portfolio.equity) a b c 2020-01-01 98.654463 98.996498 98.654463 2020-01-02 197.308925 98.989493 195.618838 2020-01-03 295.963388 99.978985 193.939564 2020-01-04 197.308925 95.971980 127.840840 2020-01-05 98.654463 90.957990 126.398131 ``` """ # Get defaults if order_price is None: order_price = main_price if init_capital is None: init_capital = defaults.portfolio['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(main_price, (pd.Series, pd.DataFrame)) # Broadcast inputs main_price, order_size, order_price, fees, fixed_fees, slippage = \ reshape_fns.broadcast(main_price, order_size, order_price, fees, fixed_fees, slippage, **broadcast_kwargs, writeable=True) target_shape = (main_price.shape[0], main_price.shape[1] if main_price.ndim > 1 else 1) init_capital = np.broadcast_to(init_capital, (target_shape[1],)) # Perform calculation order_records, cash, shares = nb.simulate_from_orders_nb( target_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 wrapper = ArrayWrapper.from_obj(main_price, freq=freq) cash = wrapper.wrap(cash) shares = wrapper.wrap(shares) orders = Orders(order_records, main_price, freq=freq) if checks.is_series(main_price): init_capital = init_capital[0] else: init_capital = wrapper.wrap_reduced(init_capital) return cls(main_price, init_capital, orders, cash, shares, freq=freq, **kwargs)
def __init__(self, obj, **kwargs): if not checks.is_pandas(obj): # parent accessor obj = obj._obj self._obj = obj self._wrapper = ArrayWrapper.from_obj(obj, **kwargs)
def from_data(cls: tp.Type[DataT], data: tp.Data, tz_localize: tp.Optional[tp.TimezoneLike] = None, tz_convert: tp.Optional[tp.TimezoneLike] = None, missing_index: tp.Optional[str] = None, missing_columns: tp.Optional[str] = None, wrapper_kwargs: tp.KwargsLike = None, **kwargs) -> DataT: """Create a new `Data` instance from (aligned) data. Args: data (dict): Dictionary of array-like objects keyed by symbol. tz_localize (timezone_like): If the index is tz-naive, convert to a timezone. See `vectorbt.utils.datetime_.to_timezone`. tz_convert (timezone_like): Convert the index from one timezone to another. See `vectorbt.utils.datetime_.to_timezone`. missing_index (str): See `Data.align_index`. missing_columns (str): See `Data.align_columns`. wrapper_kwargs (dict): Keyword arguments passed to `vectorbt.base.array_wrapper.ArrayWrapper`. **kwargs: Keyword arguments passed to the `__init__` method. For defaults, see `data` in `vectorbt._settings.settings`.""" from vectorbt._settings import settings data_cfg = settings['data'] # Get global defaults if tz_localize is None: tz_localize = data_cfg['tz_localize'] if tz_convert is None: tz_convert = data_cfg['tz_convert'] if missing_index is None: missing_index = data_cfg['missing_index'] if missing_columns is None: missing_columns = data_cfg['missing_columns'] if wrapper_kwargs is None: wrapper_kwargs = {} data = data.copy() for k, v in data.items(): # Convert array to pandas if not isinstance(v, (pd.Series, pd.DataFrame)): v = np.asarray(v) if v.ndim == 1: v = pd.Series(v) else: v = pd.DataFrame(v) # Perform operations with datetime-like index if isinstance(v.index, pd.DatetimeIndex): if tz_localize is not None: if not is_tz_aware(v.index): v = v.tz_localize(to_timezone(tz_localize)) if tz_convert is not None: v = v.tz_convert(to_timezone(tz_convert)) v.index.freq = v.index.inferred_freq data[k] = v # Align index and columns data = cls.align_index(data, missing=missing_index) data = cls.align_columns(data, missing=missing_columns) # Create new instance symbols = list(data.keys()) wrapper = ArrayWrapper.from_obj(data[symbols[0]], **wrapper_kwargs) return cls( wrapper, data, tz_localize=tz_localize, tz_convert=tz_convert, missing_index=missing_index, missing_columns=missing_columns, **kwargs )
def from_order_func(cls, main_price, order_func_nb, *args, init_capital=None, freq=None, **kwargs): """Build portfolio from a custom order function. Starting with initial capital `init_capital`, iterates over shape `main_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 `main_price`, you should tile/repeat `main_price` to the desired shape. Args: main_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, float or array_like): The initial capital. Single value or value per column. freq (any): Index frequency in case `main_price.index` is not datetime-like. **kwargs: Keyword arguments passed to the `__init__` method. For defaults, see `vectorbt.defaults.portfolio`. All time series will be broadcasted together using `vectorbt.base.reshape_fns.broadcast`. At the end, they will have the same metadata. !!! note `order_func_nb` must be Numba-compiled. Example: Portfolio from buying daily: ```python-repl >>> from vectorbt.portfolio 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.orders.records) col idx size price fees side 0 0 0 10.0 1.01 1.101 0 1 0 1 10.0 2.02 1.202 0 2 0 2 10.0 3.03 1.303 0 3 0 3 10.0 2.02 1.202 0 4 0 4 10.0 1.01 1.101 0 >>> print(portfolio.equity) 2020-01-01 98.799 2020-01-02 107.397 2020-01-03 125.794 2020-01-04 94.392 2020-01-05 53.191 Name: a, dtype: float64 ``` """ # Get defaults if init_capital is None: init_capital = defaults.portfolio['init_capital'] # Perform checks checks.assert_type(main_price, (pd.Series, pd.DataFrame)) checks.assert_numba_func(order_func_nb) # Broadcast inputs target_shape = (main_price.shape[0], main_price.shape[1] if main_price.ndim > 1 else 1) init_capital = np.broadcast_to(init_capital, (target_shape[1],)) # Perform calculation order_records, cash, shares = nb.simulate_nb( target_shape, init_capital, order_func_nb, *args) # Bring to the same meta wrapper = ArrayWrapper.from_obj(main_price, freq=freq) cash = wrapper.wrap(cash) shares = wrapper.wrap(shares) orders = Orders(order_records, main_price, freq=freq) if checks.is_series(main_price): init_capital = init_capital[0] else: init_capital = wrapper.wrap_reduced(init_capital) return cls(main_price, init_capital, orders, cash, shares, freq=freq, **kwargs)