Exemplo n.º 1
0
Arquivo: nb.py Projeto: yamen/vectorbt
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
Exemplo n.º 2
0
Arquivo: nb.py Projeto: yamen/vectorbt
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