polakowo / vectorbt

Find your trading edge, using the fastest engine for backtesting, algorithmic trading, and research.

Home Page:https://vectorbt.dev

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

bug: long_only and short_only periodic returns don't sum up to long_short daily returns

andreas-vester opened this issue · comments

I created a simple test strategy (SMA crossover). I set it up so that long_only, short_only and both can be analyzed.

When analyzing the periodic returns, I stumbled upon the fact that the sum of periodic long_only and short_only returns doesn't always equal the corresponding both returns.

Here are some daily returns where the above assumption is true:

grafik

However, as soon as I introduce fees or slippage, the short_only side doesn't correspond to the both side (and we are not talking about rounding issues), while long_only looks quite reasonable.

grafik

Am I missing something? Is this a bug / incorrect computation?

Here's the code to reproduce the issue:

import numpy as np
import pandas as pd
import pytest

import vectorbt as vbt
from datetime import datetime


def compute_vbt_pf(fee: float, slippage: float) -> vbt.Portfolio:
    DIRECTIONS = ["long_only", "short_only", "both"]

    symbols = ["SPY"]
    start = datetime(2020, 12, 30)
    end = datetime(2023, 10, 31)

    prices = vbt.YFData.download(
        symbols=symbols, start=start, end=end, tz_localize=None
    )
    close = prices.get("Close")
    _close = close.vbt.tile(3)
    _close.columns = DIRECTIONS

    # compute strategy signals
    sma = vbt.MA.run(close, 10)
    signals_long = sma.close_crossed_above(sma.ma) * 1
    signals_short = sma.close_crossed_below(sma.ma) * 1
    signals_both = signals_long + (signals_short * (-1))

    signals = pd.concat([signals_long, signals_short, signals_both], axis=1)
    signals.columns = DIRECTIONS
    signals.columns.name = "direction"

    # compute trades / size
    size = np.full(shape=signals.shape, fill_value=np.nan)
    # long_only
    size[signals["long_only"] == 1, 0] = 1
    size[signals["short_only"] == 1, 0] = 0
    # short_only
    size[signals["long_only"] == 1, 1] = 0
    size[signals["short_only"] == 1, 1] = 1
    # both
    size[:, 2] = size[:, 0] - size[:, 1]

    vbt.settings["portfolio"]["fees"] = fee
    vbt.settings["portfolio"]["slippage"] = slippage

    # create portfolio object
    return vbt.Portfolio.from_orders(
        close=_close,
        size=size,
        size_type="target_percent",
        price=_close,
        direction=DIRECTIONS,
        call_seq="auto",
        cash_sharing=False,
    )


@pytest.mark.parametrize("fee", [0.0, 0.002])
@pytest.mark.parametrize("slippage", [0.0, 0.05])
def test_daily_returns_sum_up(fee: float, slippage: float) -> None:
    """
    Test if sum of daily "long_only" and "short_only" returns sum up to daily "both"
    returns.

    """
    pf = compute_vbt_pf(fee=fee, slippage=slippage)
    rets = pf.returns()

    np.testing.assert_array_almost_equal(
        rets["long_only"] + rets["short_only"], rets["both"]
    )


Fees and slippage aren't taken into account when target percentage is translated into number of shares in order to satisfy the 100% portfolio value requirement. Thus, you short buy slightly more than you can afford (you can see this when you print the allocation with pf.asset_value() / pf.value()).

@polakowo

Thus, you short buy slightly more than you can afford

I don't fully understand this. Do you mind to elaborate?

Still, no matter how you treat fees/slippage in the first place, I feel that long_only and short_only returns should add up to both returns.

Coming back to my example above (periods marked in yellow in the second dataframe), if I am only invested on the short_only side, the return in these specific periods will solely be responsible for the overall (both) portfolio return, won't it?

If my only portfolio position is to be short one asset and this asset looses 5% in a single period, then I expect my return on the short_only side to be +5%. As this is my only position, the portfolio return should be equally +5%, shouldn't it?

@polakowo Any thoughts?