Skip to content

feat: add compounded flag to calmar() for non-compounded return streams#509

Open
wavebyrd wants to merge 385 commits intoranaroussi:mainfrom
wavebyrd:add-compounded-flag-to-calmar
Open

feat: add compounded flag to calmar() for non-compounded return streams#509
wavebyrd wants to merge 385 commits intoranaroussi:mainfrom
wavebyrd:add-compounded-flag-to-calmar

Conversation

@wavebyrd
Copy link
Copy Markdown

Fixes #507

Problem

qs.stats.calmar() always uses geometric CAGR (compounded=True) as the numerator. This gives misleading results for intraday, per-trade PnL, or other non-compounded return streams where arithmetic annualized return is the correct numerator.

Changes

  • Add compounded=True parameter to calmar() in quantstats/stats.py
  • Pass it through to the underlying cagr() call, which already supports compounded=False
  • Thread the flag through in reports.py so that metrics(compounded=False) produces correct Calmar values

Fully backward-compatible -- default is compounded=True, matching the existing behavior.

Usage

# existing behavior (unchanged)
qs.stats.calmar(returns)

# for intraday / arithmetic return streams
qs.stats.calmar(returns, compounded=False)

ranaroussi and others added 30 commits October 10, 2021 15:09
…mpound value is retured in pandas resampler apply function
Match dates logic on make_index function
Adjust parameter order, fixes ranaroussi#142
fix an typo where a cumprod series, not a single compound value is returned in…
Signed-off-by: ran <ran@aroussi.com>
Signed-off-by: ran <ran@aroussi.com>
…ted, due to dataframes being unnecessarily updated
'Risk-Free Rate %'] = _pd.Series(s_rf)*100 so that it shows in the report as the right percentage (specifically in html report)
…ate-report

temporary fix for: Rf rate when not 0 metrics are incorrectly calculated due to dataframe alteration
ranaroussi and others added 28 commits August 14, 2025 23:44
- Handle Series results from DataFrame.sum() operations
- Use replace(0, NaN) for Series, scalar check for single values
- Fixes ValueError when checking truth value of Series
Fixed ValueError 'truth value of Series is ambiguous' for:
- sortino: Use replace(0, NaN) for Series downside
- outlier_win_ratio: Handle Series positive_mean
- outlier_loss_ratio: Handle Series negative_mean
- risk_return_ratio: Handle Series std
- ulcer_performance_index: Handle Series ulcer index
- serenity_index: Handle Series std and denominator
- gain_to_pain_ratio: Handle Series downside

All functions now properly return Series for DataFrame inputs
Added comprehensive DataFrame input handling fixes to prevent ValueError
When using qs.reports.html with a benchmark, payoff_ratio receives
a DataFrame containing both strategy and benchmark columns. This caused
avg_loss to return a Series, leading to 'ValueError: truth value of
Series is ambiguous' when checking if avg_loss_val == 0.

Solution: Handle both Series and scalar cases properly using isinstance
check and replace(0, NaN) for Series.
Added fix for issue ranaroussi#463 - payoff_ratio DataFrame handling
…ry_factor

Issue ranaroussi#463 was not fully resolved in 0.0.73. Fixed:
- kelly_criterion: Properly handle Series from DataFrame inputs
  (was using 'or' operator which fails on Series)
- recovery_factor: Handle Series max_dd from DataFrame inputs

Both functions now properly detect Series vs scalar and handle
accordingly. Tested with qs.reports.metrics() using benchmark.
Complete resolution of issue ranaroussi#463 with kelly_criterion and recovery_factor fixes
- Updated make_index default rebalance from '1M' to '1ME'
- Removed warning suppression in _compat.py
- Ensures clean execution with pandas 2.2.0+
- Bumped version to 0.0.75
… comparisons

Previously, benchmark yearly returns would change based on the strategy's
trading calendar due to date alignment dropping returns on non-trading days.

This fix preserves the original benchmark data before alignment and uses it
for EOY calculations, ensuring benchmark returns remain constant regardless
of which strategy they're compared against.

Changes:
- Store original benchmark data in HTML reports before alignment
- Use original benchmark for EOY comparison calculations
- Benchmark returns now consistent whether comparing PSEI vs PSEI or TEVA vs PSEI

Fixes ranaroussi#457

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
Previously, comparing data with different timezones (or timezone-aware vs
timezone-naive) would cause "Cannot compare dtypes" errors.

This fix:
- Normalizes all data to UTC then removes timezone info for consistency
- Handles timezone normalization in safe_resample, aggregate_returns, and compare
- Ensures benchmark and strategy data can be compared regardless of original timezone
- Prevents date misalignment issues like comparing 2024-01-10+0400 to 2024-01-10-0700

All timezone combinations now work correctly, enabling reliable cross-market
analysis between US, European, and Asian markets.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
Updated all pct_change() calls to explicitly use fill_method=None to prevent
the deprecation warning: "The default fill_method='pad' in Series.pct_change
is deprecated and will be removed in a future version."

Changes:
- utils.py: Fixed pct_change() in _prepare_returns, download_returns, and _prepare_benchmark
- _plotting/wrappers.py: Fixed pct_change() in portfolio calculations
- All instances now use fill_method=None with explicit fillna(0) where needed

This ensures compatibility with pandas 2.x and future versions.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
The conditional_value_at_risk function now correctly handles DataFrame inputs by dropping NaN values after filtering returns below the VaR threshold. This prevents NaN values from skewing the mean calculation and ensures consistent CVaR results for both Series and DataFrame inputs.

Additionally, new comprehensive tests were added to verify the fix and to cover related issues ranaroussi#467 and ranaroussi#468, ensuring the CVaR calculation and report generation work correctly without errors.

Co-authored-by: terragon-labs[bot] <terragon-labs[bot]@users.noreply.github.com>
- Bumped version from 0.0.76 to 0.0.77
- Updated CHANGELOG.md to document fixes for issues ranaroussi#467 and ranaroussi#468
- Issue ranaroussi#467: CVaR calculation now works correctly with DataFrame inputs
- Issue ranaroussi#468: Confirmed already resolved in previous versions

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
## Packaging & Dependencies
- Migrated from setup.py to pyproject.toml
- Python 3.10+ minimum (dropped 3.8, 3.9)
- pandas 2.0+, numpy 1.24+, scipy 1.11+, seaborn 0.13+, matplotlib 3.7+

## New Features
- Monte Carlo simulation module (_montecarlo.py)
  - Confidence intervals for returns, Sharpe, drawdown, CAGR
  - Bootstrap resampling with configurable simulations
  - Path simulation and Monte Carlo plots
- Parameters table in HTML reports (issue ranaroussi#472)
- Average drawdown line in underwater plot (issue ranaroussi#489)
- Chrome dark mode support (issue ranaroussi#492)

## Bug Fixes (15+ issues resolved)
- ranaroussi#491: HTML report with benchmark Series
- ranaroussi#486: reports.metrics vs reports.full inconsistency
- ranaroussi#485: Benchmark Omega always same as strategy
- ranaroussi#484: make_index incorrect calculation
- ranaroussi#481: NaN in EOY Returns vs Benchmark
- ranaroussi#480: Inconsistent metrics benchmark vs return-only
- ranaroussi#479: EOY Returns vs Benchmark section issues
- ranaroussi#475: Double "%" in HTML report
- ranaroussi#477: Noisy variance warning messages
- ranaroussi#467: CVaR calculation for DataFrame inputs
- Fixed timezone normalization for cross-market comparisons
- Fixed FutureWarning for deprecated pandas pct_change()
- Fixed parameters kwarg not being used in html reports

## Code Quality
- Added comprehensive type hints to 20+ stats.py functions
- Uses Python 3.10+ union syntax (X | Y)
- Added py.typed marker for PEP 561
- Simplified _compat.py and _numpy_compat.py (removed obsolete checks)
- README converted from RST to Markdown

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…ates

feat: 2026 Modernization - Python 3.10+, Monte Carlo, Bug Fixes
- Added section explaining that metrics like win_rate, consecutive_wins,
  payoff_ratio, and profit_factor are period-based (not trade-based)
- Updated Python version badge to 3.10+
- Updated requirements section to match pyproject.toml

Addresses concerns raised in ranaroussi#493

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…etrics

docs: clarify period-based vs trade-based metrics
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](actions/checkout@v4...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
These test files were created during development/debugging and should
not be in the repository root. The proper tests are in tests/ directory.

Removed:
- test_comprehensive.py
- test_fixes.py
- test_issue_467.py
- test_issue_468.py

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* chore: remove old test files from root directory

These test files were created during development/debugging and should
not be in the repository root. The proper tests are in tests/ directory.

Removed:
- test_comprehensive.py
- test_fixes.py
- test_issue_467.py
- test_issue_468.py

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* docs: add developer instructions to PROJECT.md

- Added project structure documentation
- Added testing and code quality commands
- Added common tasks (new metrics, deps, version bump)
- Added gotchas section

🤖 Generated with Claude Code

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* chore: remove old test files from root directory

These test files were created during development/debugging and should
not be in the repository root. The proper tests are in tests/ directory.

Removed:
- test_comprehensive.py
- test_fixes.py
- test_issue_467.py
- test_issue_468.py

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* docs: add developer instructions to PROJECT.md

- Added project structure documentation
- Added testing and code quality commands
- Added common tasks (new metrics, deps, version bump)
- Added gotchas section

🤖 Generated with Claude Code

* docs: add DeepWiki badge to README

🤖 Generated with Claude Code

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
…ussi#500)

* chore: remove old test files from root directory

These test files were created during development/debugging and should
not be in the repository root. The proper tests are in tests/ directory.

Removed:
- test_comprehensive.py
- test_fixes.py
- test_issue_467.py
- test_issue_468.py

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* docs: add developer instructions to PROJECT.md

- Added project structure documentation
- Added testing and code quality commands
- Added common tasks (new metrics, deps, version bump)
- Added gotchas section

🤖 Generated with Claude Code

* docs: add DeepWiki badge to README

🤖 Generated with Claude Code

* fix: resolve circular import error on import (ranaroussi#499)

Defer stats import in utils.py to avoid circular dependency:
- __init__.py imports stats, utils
- stats.py imports utils
- utils.py was importing stats at module level (circular!)

Moved stats import inside to_prices() and group_returns() functions
where it's actually needed.

Fixes ranaroussi#499

🤖 Generated with Claude Code

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Hotfix release for circular import bug (ranaroussi#499)

🤖 Generated with Claude Code
- Implemented lazy imports in utils.py, reports.py, _plotting/core.py, _plotting/wrappers.py
- Removed quantstats/stats.py from .gitignore (was incorrectly excluding it from builds)
- All modules now defer stats/utils imports until actually needed

Fixes ranaroussi#499, ranaroussi#501

🤖 Generated with Claude Code
Fix NameError in reports.full() caused by accidental find-and-replace
during v0.0.80 lazy import refactoring.

🤖 Generated with Claude Code
…ranaroussi#502)

- Fixed circular import errors that broke import quantstats
- Fixed NameError in reports.full()
- Fixed reports.html() to work without output file (opens in browser)
- Fixed profit_ratio() DataFrame handling
- Removed dark mode CSS from HTML report
- Improved HTML report header (Compounded/matched dates conditionals)
- Added new metrics to full report (UPI, RAR, Risk-Return Ratio, etc.)
- Added terminal output parameters table
- Added comprehensive test suite (125 tests)
Pass a `compounded` parameter through to the underlying CAGR calculation
so that intraday and arithmetic return streams get the correct annualized
return in the numerator instead of geometric CAGR.

Fixes ranaroussi#507
@wavebyrd wavebyrd force-pushed the add-compounded-flag-to-calmar branch from eeae74e to f76ef15 Compare March 13, 2026 21:38
@wavebyrd
Copy link
Copy Markdown
Author

Ping on this compounded flag feature for calmar(). Allows correct calculation for non-compounded returns. Ready for review!

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.

Add compounded flag to qs.stats.calmar() for intraday / non-compounded return streams