def simulate_row_wise_nb(target_shape, init_capital, row_prep_func_nb, order_func_nb, *args): """Simulate a portfolio by iterating over rows and generating and filling orders. As opposed to `simulate_nb`, iterates using C-like index order, with the rows changing fastest, and the columns changing slowest. `row_prep_func_nb` must accept the current row context `vectorbt.portfolio.enums.RowContext`, and `*args`. Should return a tuple of any content. `order_func_nb` must accept the current order context `vectorbt.portfolio.enums.OrderContext`, unpacked result of `row_prep_func_nb`, and `*args`. Should either return an `vectorbt.portfolio.enums.Order` tuple or `None` to do nothing. !!! note This function allows sharing information between columns. This allows complex logic such as rebalancing. Example: Simulate random rebalancing. Note, however, that columns do not share the same capital. ```python-repl >>> import numpy as np >>> import pandas as pd >>> from numba import njit >>> from vectorbt.portfolio.nb import simulate_row_wise_nb >>> from vectorbt.portfolio.enums import Order, SizeType >>> price = np.asarray([ ... [1, 5, 1], ... [2, 4, 2], ... [3, 3, 3], ... [4, 2, 2], ... [5, 1, 1] ... ]) >>> init_capital = np.full(3, 100) >>> fees = 0.001 >>> fixed_fees = 1 >>> slippage = 0.001 >>> @njit ... def row_prep_func_nb(rc): ... np.random.seed(rc.i) ... w = np.random.uniform(0, 1, size=rc.target_shape[1]) ... return (w / np.sum(w),) >>> @njit ... def order_func_nb(oc, w): ... current_value = oc.run_cash / price[oc.i, oc.col] + oc.run_shares ... target_size = w[oc.col] * current_value ... return Order(target_size - oc.run_shares, SizeType.Shares, ... price[oc.i, oc.col], fees, fixed_fees, slippage) >>> order_records, cash, shares = simulate_row_wise_nb( ... price.shape, init_capital, row_prep_func_nb, order_func_nb) >>> pd.DataFrame.from_records(order_records) col idx size price fees side 0 0 0 29.399155 1.001 1.029429 0 1 0 1 5.872746 1.998 1.011734 1 2 0 2 1.855144 2.997 1.005560 1 3 0 3 6.433713 3.996 1.025709 1 4 0 4 0.796768 4.995 1.003980 1 5 1 0 7.662334 5.005 1.038350 0 6 1 1 6.785973 4.004 1.027171 0 7 1 2 13.801094 2.997 1.041362 1 8 1 3 16.265081 2.002 1.032563 0 9 1 4 4.578725 0.999 1.004574 1 10 2 0 32.289173 1.001 1.032321 0 11 2 1 32.282575 1.998 1.064501 1 12 2 2 23.557854 3.003 1.070744 0 13 2 3 13.673091 1.998 1.027319 1 14 2 4 27.049616 1.001 1.027077 0 >>> cash [[ 69.5420172 60.61166607 66.64621673] [ 80.26402911 32.41346024 130.08230128] [ 84.81833559 72.73397843 58.26732111] [109.50174358 39.13872328 84.55883717] [112.47761836 42.70829586 56.45509534]] >>> shares [[2.93991551e+01 7.66233445e+00 3.22891726e+01] [2.35264095e+01 1.44483072e+01 6.59749521e-03] [2.16712656e+01 6.47212729e-01 2.35644516e+01] [1.52375526e+01 1.69122939e+01 9.89136108e+00] [1.44407849e+01 1.23335684e+01 3.69409766e+01]] ``` """ order_records = np.empty(target_shape[0] * target_shape[1], dtype=order_dt) j = 0 cash = np.empty(target_shape, dtype=np.float_) shares = np.empty(target_shape, dtype=np.float_) for i in range(target_shape[0]): # Run a row preparation function and pass the result to each order function row_context = RowContext( i, target_shape, init_capital, order_records[:j], # not sorted! cash, shares) prep_result = row_prep_func_nb(row_context, *args) for col in range(target_shape[1]): if i == 0: run_cash = float( flex_select_nb(0, col, init_capital, is_2d=True)) run_shares = 0. else: run_cash = cash[i - 1, col] run_shares = shares[i - 1, col] # Generate the next order or None to do nothing order_context = OrderContext( col, i, target_shape, init_capital, order_records[:j], # not sorted! cash, shares, run_cash, run_shares) order = order_func_nb(order_context, *prep_result, *args) if order is not None: # Fill the order run_cash, run_shares, filled_order = fill_order_nb( run_cash, run_shares, order) # Add a new record if filled_order is not None: order_records[j]['col'] = col order_records[j]['idx'] = i order_records[j]['size'] = filled_order.size order_records[j]['price'] = filled_order.price order_records[j]['fees'] = filled_order.fees order_records[j]['side'] = filled_order.side j += 1 # Populate cash and shares cash[i, col], shares[i, col] = run_cash, run_shares # Order records are not sorted yet order_records = order_records[:j] return order_records[np.argsort(order_records['col'])], cash, shares
def simulate_nb(target_shape, init_capital, order_func_nb, *args): """Simulate a portfolio by iterating over columns and generating and filling orders. Starting with initial capital `init_capital`, iterates over each column in shape `target_shape`, and for each data point, generates an order using `order_func_nb`. Tries then to fulfill that order. If unsuccessful due to insufficient cash/shares, orders the available fraction. Updates then the current cash and shares balance. Returns order records of layout `vectorbt.records.enums.order_dt`, but also cash and shares as time series. `order_func_nb` must accept the current order context `vectorbt.portfolio.enums.OrderContext`, and `*args`. Should either return an `vectorbt.portfolio.enums.Order` tuple or `None` to do nothing. !!! note This function assumes that all columns are independent of each other. Since iteration happens over columns, all columns next to the current one will be empty. Accessing these columns will not trigger any errors or warnings, but provide you with arbitrary data (see [numpy.empty](https://numpy.org/doc/stable/reference/generated/numpy.empty.html)). !!! warning In some cases, passing large arrays as `*args` can negatively impact performance. What can help is accessing arrays from `order_func_nb` as non-local variables as we do in the example below. Example: Simulate a basic buy-and-hold strategy: ```python-repl >>> import numpy as np >>> import pandas as pd >>> from numba import njit >>> from vectorbt.portfolio.nb import simulate_nb >>> from vectorbt.portfolio.enums import Order, SizeType >>> price = np.asarray([ ... [1, 5, 1], ... [2, 4, 2], ... [3, 3, 3], ... [4, 2, 2], ... [5, 1, 1] ... ]) >>> init_capital = np.full(3, 100) >>> fees = 0.001 >>> fixed_fees = 1 >>> slippage = 0.001 >>> @njit ... def order_func_nb(oc): ... return Order(np.inf if oc.i == 0 else 0, SizeType.Shares, ... price[oc.i, oc.col], fees, fixed_fees, slippage) >>> order_records, cash, shares = simulate_nb( ... price.shape, init_capital, order_func_nb) >>> pd.DataFrame.from_records(order_records) col idx size price fees side 0 0 0 98.802297 1.001 1.098901 0 1 1 0 19.760459 5.005 1.098901 0 2 2 0 98.802297 1.001 1.098901 0 >>> cash [[0. 0. 0.] [0. 0. 0.] [0. 0. 0.] [0. 0. 0.] [0. 0. 0.]] >>> shares [[98.8022966 19.76045932 98.8022966 ] [98.8022966 19.76045932 98.8022966 ] [98.8022966 19.76045932 98.8022966 ] [98.8022966 19.76045932 98.8022966 ] [98.8022966 19.76045932 98.8022966 ]] ``` """ order_records = np.empty(target_shape[0] * target_shape[1], dtype=order_dt) j = 0 cash = np.empty(target_shape, dtype=np.float_) shares = np.empty(target_shape, dtype=np.float_) for col in range(target_shape[1]): run_cash = float(flex_select_nb(0, col, init_capital, is_2d=True)) run_shares = 0. for i in range(target_shape[0]): # Generate the next order or None to do nothing order_context = OrderContext(col, i, target_shape, init_capital, order_records[:j], cash, shares, run_cash, run_shares) order = order_func_nb(order_context, *args) if order is not None: # Fill the order run_cash, run_shares, filled_order = fill_order_nb( run_cash, run_shares, order) # Add a new record if filled_order is not None: order_records[j]['col'] = col order_records[j]['idx'] = i order_records[j]['size'] = filled_order.size order_records[j]['price'] = filled_order.price order_records[j]['fees'] = filled_order.fees order_records[j]['side'] = filled_order.side j += 1 # Populate cash and shares cash[i, col], shares[i, col] = run_cash, run_shares return order_records[:j], cash, shares