Equity Curve: A Python Library for Professional Strategy Analysis

Tools Documentation

equity-curve is a Python library for quantitative analysts and portfolio managers. It covers the full strategy analysis pipeline — from raw NAV data to interactive dashboards — with 30+ risk metrics, econometric tests, and Plotly visualisations.

Equity Curve banner Image
Antoine Perrin Profile Picture

Antoine

CEO - CodeMarketLabs

2026-03-10

equity-curve: A Python Library for Professional Strategy Analysis

equity-curve is a Python library designed for quantitative analysts and portfolio managers who need a rigorous, modular framework to evaluate trading strategies. Built on top of pandas and plotly, it covers the full analysis pipeline — from raw NAV data to publication-ready dashboards — with a consistent API and professional-grade visualisations. Every module is independently usable: you can call a single ratio function, run a battery of econometric tests, or generate a full interactive dashboard with one line.

What the library provides

  • Core data structures with built-in validation for price series and portfolio data.
  • 30+ statistical measures: skewness, kurtosis, entropy, downside/upside deviation, IQR, MAD.
  • Performance metrics: WTD, MTD, YTD, CAGR, since-inception, monthly track record.
  • Risk ratios: Sharpe, Sortino, Calmar, Omega, Martin, Burke, VaR, CVaR, Jensen Alpha, Treynor, Up/Down Capture.
  • Econometric tests: stationarity (ADF+KPSS), normality (JB+Shapiro), autocorrelation, Hurst, Variance Ratio, BDS, cointegration, Granger.
  • Interactive Plotly dashboards exportable to HTML or PNG, dark and light themes.

1. Core Data Structures

The library is built around three classes. PriceSeries is the abstract base — it validates the index, sorts data, and warns on duplicates. TimeSeries extends it with return computation (normal or log). PortfolioTs enforces the three-column contract required by all downstream functions: strategy, benchmark, and risk_free.

python
import pandas as pd
from equity_curve.utils.financial_ts import TimeSeries, PortfolioTs

# --- Single series ---
nav = pd.Series([100, 101, 99, 103, 106], index=pd.date_range('2024-01-01', periods=5))
ts = TimeSeries(nav)

print(ts.get_returns('normal'))   # pct_change, dropna
print(ts.get_returns('log'))      # log(p_t / p_t-1), dropna
print(ts)
# PriceSeries(name=None, start=2024-01-01, end=2024-01-05, n=5)

# --- Portfolio (required for all ratio / performance functions) ---
df = pd.DataFrame({
    'strategy':  [100, 102, 101, 105, 108],
    'benchmark': [100, 101, 100, 103, 105],
    'risk_free': [100, 100.01, 100.02, 100.03, 100.04]
}, index=pd.date_range('2024-01-01', periods=5))

portfolio = PortfolioTs(df)
print(portfolio)
# PriceSeries(columns=['strategy', 'benchmark', 'risk_free'], start=2024-01-01, end=2024-01-05, n=5)

# Missing column raises immediately
try:
    PortfolioTs(df.drop(columns=['risk_free']))
except ValueError as e:
    print(e)  # Missing required columns: ['risk_free']

2. Configuration & Theming

config.py centralises all visual constants. Strategy and benchmark colours are defined once and used everywhere — in charts, tables, and annotations. The THEMES dict controls the full Plotly appearance. Every chart function accepts a mode parameter ('dark' or 'light') so you can switch without touching any other code.

python
# config.py — customise to match your brand
COLOR_STRAT = '#007668'   # teal green
COLOR_BENCH = '#b20462'   # magenta
COLOR_RF    = '#ffde59'   # yellow dashed
COLOR_RED   = '#ef0032'
COLOR_GREEN = '#00ef4a'

THEMES = {
    'dark': {
        'TEMPLATE':   'plotly_dark',
        'BG_COLOR':   '#0f1117',
        'COLOR_GRID': 'rgba(255,255,255,0.06)',
        'COLOR_TEXT': '#ffffff'
    },
    'light': {
        'TEMPLATE':   'plotly_white',
        'BG_COLOR':   '#ffffff',
        'COLOR_GRID': 'rgba(0,0,0,0.06)',
        'COLOR_TEXT': '#000000'
    }
}

# Usage — pass mode to any chart
from equity_curve.graphs import plot_performance
plot_performance(perf, mode='dark').show()
plot_performance(perf, mode='light').show()

3. Performance Metrics

The Performances class wraps a PortfolioTs with a computation_date and a basis (number of periods per year). It acts as the single object passed to all metric functions. Period returns handle calendar edge cases: if the exact target date is missing, the function looks back up to 5 days for a valid observation.

python
from datetime import datetime
from equity_curve.performances import (
    Performances, cagr, wtd_perf, mtd_perf, ytd_perf,
    one_month_perf, three_month_perf, since_inception_perf,
    track_record, seasonality_analysis, hit_ratio, avg_win_loss
)

perf = Performances(
    strat=portfolio,
    computation_date=datetime(2024, 12, 31),
    period_per_year=252   # trading days
)

# Period returns — all return {'strategy': x, 'benchmark': y, 'risk_free': z}
print(wtd_perf(perf))              # week-to-date
print(mtd_perf(perf))              # month-to-date
print(ytd_perf(perf))              # year-to-date
print(three_month_perf(perf))      # rolling 3 months
print(since_inception_perf(perf))  # full period
print(cagr(perf))                  # compound annual growth rate

# Track record — years x months pivot
print(track_record(perf))

# Seasonality
monthly = seasonality_analysis(perf, frequency='M')   # Jan–Dec avg returns
daily   = seasonality_analysis(perf, frequency='D')   # Mon–Fri avg returns

# Win/Loss stats
print(hit_ratio(perf))      # 0.54 — 54% of days positive
print(avg_win_loss(perf))   # {'avg_win': 0.008, 'avg_loss': -0.006}
Graphs des performances relatives
Graphs des performances relatives

4. Statistics

The statistics module operates on any TimeSeries object and covers the full spectrum of distributional analysis. All functions accept an r_type parameter ('normal' or 'log') and an optional annualization flag. They work on both Series and DataFrame inputs.

python
from equity_curve.statistics import (
    mean_returns, standard_deviation, skewness, kurtosis,
    downside_deviation, upside_deviation, iqr, mad,
    entropy, percentile, zscore, coefficient_of_variation
)

ts = TimeSeries(df['strategy'])

# Moments
print(mean_returns(ts, annualized=True))    # annualised mean return
print(standard_deviation(ts, annualized=True))  # annualised vol
print(skewness(ts))       # -0.42 — slight left skew
print(kurtosis(ts))       # 3.81  — excess kurtosis, fat tails

# Tail & dispersion
print(downside_deviation(ts))   # sqrt(mean of negative squared returns)
print(upside_deviation(ts))     # sqrt(mean of positive squared returns)
print(iqr(ts))                  # Q75 - Q25
print(mad(ts))                  # median absolute deviation
print(percentile(ts, 0.05))     # 5th percentile — proxy for VaR

# Distribution quality
print(entropy(ts, bins=50))               # Shannon entropy
print(coefficient_of_variation(ts))       # std / |mean|
print(zscore(ts, prices=False).tail(3))   # z-score on returns

5. Risk Ratios

pm_ratios.py covers every major risk-adjusted performance metric. Functions return a dict keyed by column name so strategy and benchmark are always computed together. The all_ratios() function computes everything at once and includes a plain-English description of each metric.

python
from equity_curve.pm_ratios import (
    sharpe_ratio, sortino_ratio, calmar_ratio, omega_ratio,
    martin_ratio, burke_ratio, gain_pain_ratio,
    max_drawdown, ulcer_index, pain_index,
    var_histo, cvar_histo,
    jensen_alpha, treynor_ratio, information_ratio,
    tracking_error, up_down_capture, linear_regression,
    all_ratios
)

# Return-based ratios
print(sharpe_ratio(perf))     # {'strategy': 0.72, 'benchmark': 0.61}
print(sortino_ratio(perf))    # only penalises downside vol
print(calmar_ratio(perf))     # CAGR / abs(max drawdown)
print(omega_ratio(perf))      # gains / losses above threshold=0
print(martin_ratio(perf))     # CAGR / Ulcer Index
print(burke_ratio(perf))      # CAGR / sqrt(sum of squared drawdowns)
print(gain_pain_ratio(perf))  # sum(returns) / sum(abs negative returns)

# Drawdown metrics
print(max_drawdown(perf))   # {'strategy': -0.184, 'benchmark': -0.241}
print(ulcer_index(perf))    # RMS of all drawdowns
print(pain_index(perf))     # mean absolute drawdown

# Tail risk
print(var_histo(perf, 0.95))    # 95% historical VaR
print(cvar_histo(perf, 0.95))   # Expected Shortfall
print(var_histo(perf, 0.99))    # 99% VaR

# Benchmark-relative
print(jensen_alpha(perf))       # annualised CAPM alpha
print(treynor_ratio(perf))      # excess return / beta
print(information_ratio(perf))  # active return / tracking error
print(tracking_error(perf))     # std(strategy - benchmark)
print(up_down_capture(perf))    # {'up_capture': 0.88, 'down_capture': 0.71, 'capture_ratio': 1.24}

# CAPM regression
reg = linear_regression(perf)
print(reg)  # {'alpha': 0.0003, 'beta': 0.82, 'r2': 0.76}

# All at once with descriptions
ratios = all_ratios(perf)
print(ratios['sharpe_ratio']['value'])        # {'strategy': 0.72, ...}
print(ratios['sharpe_ratio']['description'])  # human-readable explanation
Table complète des métriques de rendement et de risque
Table complète des métriques de rendement et de risque

6. Econometric Tests

The econometric_tests module provides a consistent interface for hypothesis testing. All functions return the same structured dict: stat, p-value, test_results (a human-readable verdict), and a details block with description, H0, and H1. Univariate tests work on both Series and DataFrame inputs. Multivariate tests require exactly 2 columns.

python
from equity_curve.econometric_tests.univariate import (
    stationarity_test, normality_test, autocorrelation_test,
    heteroscedasticity_test, hurst_exponent_test,
    variance_ratio_test, bds_test
)
from equity_curve.econometric_tests.multivariate import (
    engle_granger_test, granger_test, johansen_test,
    correlation_test, chow_test
)

# --- Univariate ---
res = stationarity_test(ts)
print(res)  # ADF + KPSS combined → 'Stationary' / 'Unit Root' / 'Trend Stationary'

res = normality_test(ts)
print(res['strategy']['test_results'])  # 'Not Normal'
print(res['strategy']['p-value'])       # {'JB-p-value': 0.001, 'SW-p-value': 0.003}

res = autocorrelation_test(ts, lags=10)
print(res)  # 'Positive Autocorr (Momentum)' / 'No Autocorr (White Noise)'

res = hurst_exponent_test(ts)
print(res['strategy']['stat'])          # 0.53 — slightly trending
print(res['strategy']['test_results'])  # 'Trending (Persistent Memory)'

res = variance_ratio_test(ts, lags=[2, 5, 10, 20])
print(res)  # per lag: 'Momentum' / 'Mean Reversion' / 'Random Walk'

# --- Multivariate ---
pair = TimeSeries(df[['strategy', 'benchmark']])

print(engle_granger_test(pair))   # → 'Cointegrated' if p < 0.05
print(granger_test(pair, max_lag=5))  # does strategy Granger-cause benchmark?
print(correlation_test(pair, method='pearson'))   # linear correlation
print(correlation_test(pair, method='spearman'))  # monotonic correlation

# Structural break at a known date
from datetime import datetime
print(chow_test(pair, split_date=datetime(2022, 1, 1)))  # 'Structural Break' or 'Stable'

7. Visualisation & Dashboard

graphs.py exposes a standalone Plotly figure for every analysis module, and a dashboard() function that assembles all of them into a single scrollable figure. The _add() helper intelligently routes xy and table traces to the correct subplot type, so the layout is fully automatic. Export to HTML preserves full interactivity; PNG export uses kaleido.

python
from equity_curve.graphs import (
    plot_performance, plot_underwater, plot_drawdowns,
    plot_rolling_sharpe, plot_rolling_volatility, plot_rolling_beta, plot_rolling_correlation,
    plot_annual_returns, plot_returns_histogram, plot_seasonality,
    table_perf, table_ratios, table_track_record, table_returns_stats,
    dashboard, export_dashboard, export_dashboard_png
)

# Individual charts
plot_performance(perf, mode='dark').show()
plot_drawdowns(perf, top_n=5, mode='dark').show()
plot_rolling_sharpe(perf, window=90, mode='dark').show()
plot_seasonality(perf, frequency='M', mode='dark').show()   # monthly avg returns
plot_seasonality(perf, frequency='D', mode='dark').show()   # day-of-week avg returns

# Individual tables
table_perf(perf, mode='dark').show()
table_ratios(perf, mode='dark').show()
table_track_record(perf, mode='dark').show()

# Full dashboard — assembles all charts + tables
fig = dashboard(
    perf,
    window_roll=90,    # rolling window in days
    top_dd=5,          # number of top drawdowns to highlight
    mode='dark',
    title='Global Portfolio Strategy'
)
fig.show()  # opens in browser, scrollable

# Export
export_dashboard(perf, 'report.html', title='Global Portfolio Strategy')
export_dashboard_png(perf, 'report.png', title='Global Portfolio Strategy')
Dashboard Complet
Dashboard Complet

Get equity-curve

equity-curve is available as a standalone Python package. Whether you are running a systematic strategy, analysing a discretionary fund, or building performance reports for clients, the library gives you a production-ready analytical stack in a few lines of code.

What you get

  • Full source code with typed, documented functions.
  • Dark and light theme dashboards exportable to HTML and PNG.
  • 30+ risk and performance metrics with strategy vs benchmark comparison.
  • Full econometric test suite: univariate and multivariate.
  • Lifetime updates — new ratios and charts added regularly.
What data format does equity-curve require?

A pandas DataFrame with a DatetimeIndex and three columns: strategy, benchmark, and risk_free — all as price/NAV series starting at any base value (e.g. 100). The index must be parseable as dates; the library will convert strings automatically.

Can I use it with daily, weekly or monthly data?

Yes. Set period_per_year accordingly when instantiating Performances: 252 for trading days, 365 for calendar days, 52 for weekly, 12 for monthly. All annualisation in ratios and rolling charts uses this basis automatically.

Does it work in Jupyter?

Yes. All charts are Plotly figures and render inline in JupyterLab or classic notebooks. For the full dashboard, fig.show() opens it in your browser where the large figure is natively scrollable.

Can I use only part of the library without the rest?

Yes. Every module is independently importable. You can use only pm_ratios for ratio computations, only econometric_tests for hypothesis testing, or only graphs for a specific chart — without instantiating the full Performances object.

How do I customise chart colours and themes?

Edit config.py to set COLOR_STRAT, COLOR_BENCH, and the THEMES dict. Every chart picks up these values automatically via the mode parameter. No other changes are needed.