Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
andreas-vester opened this issue Nov 9, 2023 · 4 comments

Comments

@andreas-vester
Copy link

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"]
    )


@polakowo
Copy link
Owner

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()).

@andreas-vester
Copy link
Author

@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?

@andreas-vester
Copy link
Author

@polakowo Any thoughts?

@xiandong79
Copy link

@polakowo is this a bug?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants