Skip to content

Clarify Best/Worst Trade [%] definition and add regression tests for #810 - #875

Merged
polakowo merged 2 commits into
polakowo:masterfrom
Geochelone666:fix-810-best-trade-docs
Oct 9, 2026
Merged

polakowo merged 2 commits into
polakowo:masterfrom
Geochelone666:fix-810-best-trade-docs

Conversation

@Geochelone666

Copy link
Copy Markdown
Contributor

Summary

Issue #810 reported that "Best Trade [%]" in portfolio statistics shows an incorrect value for short-only strategies. Investigation (with GPT-5.6) confirmed this is a semantic misunderstanding, not a calculation bug:

  • Best Trade [%] = max(pf.trades.returns) * 100, where each trade's return = PnL / (size × entry_price) — i.e., relative to that trade's own entry notional.
  • The reporter computed PnL / 1,000,000 (PnL relative to total portfolio initial cash), which is a different metric with a different ranking.

The short-position handling in get_trade_stats_nb (val_diff *= -1 for shorts) is correct, and the issue reporter and maintainer both confirmed the behavior is expected.

Changes

  • vectorbt/portfolio/base.py: added a clarifying comment on the best_trade/worst_trade metric definitions explaining that returns are relative to each trade's entry value, not portfolio initial cash.
  • tests/test_portfolio.py: added regression tests documenting the correct semantics for short positions:
    • A trade with larger PnL but smaller return is NOT the best trade (guards against the Metric shows incorrect value in portfolio statistics #810 confusion).
    • Best Trade [%] equals closed.returns.max() * 100.
    • Short trade return matches the definition (entry_price - exit_price) / entry_price.

No calculation changes — behavior is intentionally unchanged.

Fixes #810 (documents the resolution).

…olakowo#810

Best Trade [%] is max individual trade return where return = PnL / entry value,
not PnL / portfolio initial cash. Adds comment and regression tests documenting
this behavior for short positions.
@Geochelone666

Copy link
Copy Markdown
Contributor Author

Test evidence

Ran the new regression test plus all trade-related tests in tests/test_portfolio.py:

$ python -m pytest tests/test_portfolio.py::test_best_trade_pct_definition -v
tests/test_portfolio.py::test_best_trade_pct_definition PASSED [100%]
1 passed in 63.35s

$ python -m pytest tests/test_portfolio.py -k "trade" -q
4 passed, 178 deselected in 24.49s

Environment: fresh venv, Python 3.12, numpy/pandas/scipy/plotly/numba installed from pyproject.toml dependencies.

To clarify the semantics (issue #810): Best Trade [%] is the maximum of individual trade returns, where return = PnL / trade entry value — not PnL / portfolio init_cash. This PR only clarifies the docs and adds regression tests; no calculation logic was changed.

@arhancanli

Copy link
Copy Markdown

The definition in the new comment and docstrings is right for the zero-fee case, but the tests never leave it, and the docstring "return = trade PnL / trade entry value" can mislead once fees exist. In get_trade_stats_nb (portfolio/nb.py ~L5711):

entry_val = size * entry_price
...
pnl = val_diff - entry_fees - exit_fees
ret = pnl / entry_val

So the numerator is net of both fees and the denominator is the gross size * entry_price, with entry_fees not added to it. Two consequences worth pinning in a test: a short trade's return is (entry - exit)/entry - (entry_fees + exit_fees)/(size*entry), not the plain price ratio asserted in test_short_trade_return_math; and a trade can rank first by PnL, second by return, and flip again after fees if the fee rate differs by size. Both tests set fees to 0 (test_best_trade_pct_definition relies on the default), so a change to the denominator, say to include entry fees, would pass.

I'd add one case with fees=0.001 and assert Return == (pnl)/(size*entry_price) against hand-computed numbers, and say "net of fees, relative to gross entry notional" in the comment.

Smaller one: the identity stats()["Best Trade [%]"] == trades.closed.returns.max()*100 holds because the default settings use closed trades; with incl_open=True it will not. The test has no open trade, so it can't tell the difference.

Read from the diff and nb.py on master, not run.

@Geochelone666

Copy link
Copy Markdown
Contributor Author

Thanks for the helpful review, @arhancanli! Both points are addressed in f5a705e:

  • Added a fees=0.001 case with hand-computed PnL, verifying Return == pnl / (size * entry_price). Updated the comment and docstrings to say "net of fees, relative to gross entry notional".
  • Added tests for both incl_open=True and incl_open=False, covering Best/Worst Trade [%] with open trades.

Relevant tests: 7 passed, 178 deselected.

@polakowo polakowo closed this Oct 9, 2026
@polakowo polakowo reopened this Oct 9, 2026

@polakowo polakowo left a comment •

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the clarification and tests! The definitions are correct, and the portfolio tests passed. Looks good.

@polakowo
polakowo merged commit 1520602 into polakowo:master Oct 9, 2026
1 check passed
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

Successfully merging this pull request may close these issues.

Metric shows incorrect value in portfolio statistics

3 participants