Skip to content

Strategy Analysis Modules

Cumulative return and Sharpe ratio alone miss important risk dimensions. The finlab.analysis module adds 6 built-in tools covering liquidity risk, volatility, and period stability.

Overview

FinLab includes the following 6 analysis modules:

Module Purpose Use Case
MaeMfeAnalysis Volatility analysis (MAE/MFE) Optimizing stop-loss/take-profit, understanding strategy characteristics
PeriodStatsAnalysis Period stability analysis Reviewing yearly, monthly, and recent performance
InequalityAnalysis Factor threshold analysis Checking whether filtering trades on a factor improves returns
AlphaBetaAnalysis Alpha/Beta analysis Measuring excess return and market correlation
DrawdownAnalysis Drawdown analysis Understanding max drawdown timing and magnitude
LiquidityAnalysis Liquidity risk assessment (Taiwan market only) Large capital strategies, low-liquidity assets

Basic Usage

All analysis modules use the same calling convention:

from finlab import data
from finlab.backtest import sim

# Complete backtest
data.set_market('us')
with data.us_universe(index='SP500'):
    close = data.get('price:adj_close')
position = close > close.average(20)
report = sim(position, resample='M')

# Run analysis - Method 1: Using module name (string)
report.run_analysis('MaeMfeAnalysis', violinmode='group')

# Run analysis - Method 2: Using module instance
from finlab.analysis.maeMfeAnalysis import MaeMfeAnalysis

report.run_analysis(MaeMfeAnalysis(violinmode='group'))

Method 1 is recommended

Passing the module name as a string is more concise, and FinLab handles module loading automatically.

1. MaeMfeAnalysis - Volatility Analysis

Description

MAE/MFE analysis is the core tool for optimizing stop-loss/take-profit:

  • MAE (Maximum Adverse Excursion): Maximum adverse movement during holding period (max loss)
  • BMFE (Before-MAE MFE): Maximum favorable movement before MAE occurred
  • GMFE (Global MFE): Maximum favorable movement during holding period (max profit)
  • MDD (Maximum Drawdown): Maximum drawdown during holding period

The 12 subplots show each trade's volatility profile.

Parameters

MaeMfeAnalysis(
    violinmode='group',  # Violin plot mode: 'group' or 'overlay'
    mfe_scatter_x='mae',  # MFE scatter plot X-axis: 'mae' or 'return'
)

Usage Example

The chart below is the MAE/MFE analysis of the quick-start strategy from Backtesting:

from finlab import data
from finlab import backtest

data.set_market('us')
with data.us_universe(index='SP500'):
    close = data.get('price:adj_close')

# Hold the 20 S&P 500 stocks with the strongest 6-month return
position = (close / close.shift(126)).is_largest(20)

# Backtest, rebalance at month-end (M)
report = backtest.sim(position, resample='M', name='S&P 500 momentum top 20')

# Method 1: Using the display_mae_mfe_analysis() shortcut
report.display_mae_mfe_analysis()

# Method 2: Using run_analysis()
report.run_analysis('MaeMfeAnalysis', violinmode='group')

12 Subplot Descriptions

Row 1

  1. Win Ratio: Trade win rate distribution (profit vs loss histogram)
  2. Blue: profitable trades, Pink: losing trades
  3. Green dashed line: average return

  4. Edge Ratio: MFE/MAE ratio at different holding periods

  5. Value > 1 means profit magnitude exceeds loss magnitude
  6. Observe the strategy's "edge time window"

  7. MAE vs Return: Relationship between MAE and final return

  8. Horizontal dashed line: MAE 75th percentile (Q3)
  9. Used for setting stop-loss points

Row 2

  1. GMFE vs MAE: Maximum profit vs maximum loss
  2. Q3 dashed lines: Range of 75% of trades' GMFE and MAE
  3. Used for evaluating take-profit potential

  4. BMFE vs MAE: Maximum profit before MAE occurred

  5. Higher values mean more opportunity to take profit before stop-loss triggers
  6. Used for optimizing "trailing stop" strategies

  7. MDD vs GMFE: Maximum drawdown vs maximum profit

  8. Orange line: MDD = GMFE baseline
  9. Points below: GMFE > MDD (good signal, trailing stop viable)
  10. Points above: MDD > GMFE (bad signal, missing profits)

Row 3

7-9. MAE/BMFE/GMFE Distribution: Distribution and density of each metric - Histogram + density curve - Blue: profitable trades, Pink: losing trades - Q3 dashed line: 75th percentile

Row 4

  1. Indices Stats (Violin Plot): Statistical distribution of 6 key metrics
  2. return, mae, bmfe, gmfe, mdd, pdays_ratio
  3. group mode: Shows profit/loss trade differences separately (recommended)
  4. overlay mode: All trades overlaid

Decision Recommendations

Optimize stop-loss/take-profit based on MAE/MFE analysis:

Setting Stop-Loss

# 1. Check MAE 75th percentile (Q3)
trades = report.get_trades()
mae_q3 = trades['mae'].quantile(0.75)

# 2. Set stop-loss slightly wider than Q3 to avoid over-stopping
stop_loss = abs(mae_q3) * 1.2

Setting Take-Profit

# Check GMFE 75th percentile
gmfe_q3 = trades['gmfe'].quantile(0.75)

# Set take-profit a little below Q3, ensuring most profits are captured
take_profit = gmfe_q3 * 0.8

Trailing Stop Decision

If the "MDD vs GMFE" chart shows:

  • Missed Win-profits PCT < 20%: Trailing stop is suitable
  • Missed Win-profits PCT > 40%: Trailing stop is not suitable (exits too early, missing larger gains)
# Use trailing stop (via sim()'s trail_stop parameter)
entries = close > close.average(20)
exits = close < close.average(60)
position = entries.hold_until(exit=exits, stop_loss=0.1)
# Signal after an 8 percentage-point drop from peak return
report = sim(position, resample='M', trail_stop=0.08)

2. PeriodStatsAnalysis - Period Stability Analysis

Description

Analyzes strategy performance across different periods (yearly, monthly, recent) and compares it with the benchmark (the S&P 500 for U.S. stocks) to verify stability.

Usage Example

report.run_analysis('PeriodStatsAnalysis')

Interpreting Output

Produces multiple tables including:

1. Overall Stats

One column for the benchmark and one for the strategy. Each metric (calmar_ratio, sortino_ratio, sharpe_ratio, profit_factor, tail_ratio, return, volatility) is computed from daily, monthly and yearly returns, in rows such as ('overall_daily', 'sharpe_ratio') and ('overall_monthly', 'return').

2. Yearly Stats

Heatmap showing the same 7 metrics per year.

3. Recent Stats

Shows performance for M (month), Q (quarter), HY (half-year), Y (year), 3Y (three years).

Decision Recommendations

  • Strategy sharpe_ratio < benchmark: Risk-adjusted return is worse than the market; improvement needed
  • Some years perform very poorly: Check if specific market conditions were encountered (e.g., bear market)
  • Recent performance significantly declined: Strategy may be failing; re-optimization needed

3. InequalityAnalysis - Factor Threshold Analysis

Description

Checks how trade returns change if you only keep trades whose factor value was below or above a threshold x, for every threshold. Trades are sorted by the factor value on the entry signal date. The upper charts show the mean return of the trades kept by each threshold minus the mean of all trades (<name> < x on the left, <name> > x on the right); the lower charts show the factor value at each cut against the number of trades.

Parameters

class InequalityAnalysis(
    name,  # Factor name: the trade column and chart label
    df=None,  # Factor values (dates by symbols); if None, data.get(name) is fetched
    date_type='entry_sig_date',  # 'entry_date', 'entry_sig_date', 'exit_date' or 'exit_sig_date'
    target='return',  # Any column of report.get_trades()
)

Usage Example

Do larger S&P 500 companies make better trades for a simple trend strategy?

from finlab import data
from finlab.analysis.inequalityAnalysis import InequalityAnalysis
from finlab.backtest import sim

data.set_market('us')
with data.us_universe(index='SP500'):
    close = data.get('price:adj_close')
    market_value = data.get('etl:us_market_value')

position = close > close.average(20)
report = sim(position, resample='M')
report.run_analysis(InequalityAnalysis(name='market_value', df=market_value))

Decision Recommendations

  • A curve that rises steadily as the threshold moves: The factor separates good trades from bad ones; consider adding it as a filter
  • A flat curve near zero: Filtering on this factor does not change returns
  • Thresholds that keep only a handful of trades: Results are noisy; check the trade count in the lower charts before acting

4. AlphaBetaAnalysis - Alpha/Beta Analysis

Description

Measures strategy performance relative to a benchmark index:

  • Alpha: Excess return (the strategy's unique value)
  • Beta: Market sensitivity (correlation with the market)

Usage Example

report.run_analysis('AlphaBetaAnalysis')

Interpreting Output

Shows three sections:

  1. Overall Summary -- Alpha and Beta for the whole backtest:

    Metric Description
    Alpha Annualized excess return (true contribution after removing market risk)
    Beta Market sensitivity (a Beta of 0.8 means the strategy is expected to rise 0.8% when the market rises 1%)
  2. Yearly Alpha/Beta -- Annual Alpha and Beta breakdown to observe strategy stability

  3. Recent Alpha/Beta -- Performance for full period, recent 1M, 3M, 6M, 1Y

To get raw data (dict):

result = report.run_analysis('AlphaBetaAnalysis', display=False)
# result['overall']  -> {'alpha': ..., 'beta': ...}
# result['yearly']   -> {'alpha': [...], 'beta': [...], 'year': [...]}
# result['recent']   -> {'alpha': [...], 'beta': [...], 'ndays': [...]}

Decision Recommendations

  • Alpha > 0: Strategy has excess returns; worth executing
  • Alpha < 0: Strategy underperforms the benchmark; improvement needed
  • Beta > 1.5: Strategy volatility is extremely high; high risk
  • Beta < 0: Strategy is negatively correlated with the market (suitable for hedging)

5. DrawdownAnalysis - Drawdown Analysis

Description

Lists the drawdowns of the strategy and of its benchmark:

  • The drawdown series over time
  • The 5 longest drawdowns
  • The 5 deepest drawdowns

Usage Example

result = report.run_analysis('DrawdownAnalysis')

Interpreting Output

The result is a dict with a strategy and a benchmark entry, each containing:

  1. drawdown: The drawdown series (index and values)
  2. longest_drawdown: The 5 longest drawdowns, keyed by start date, with End, Length (days) and drawdown (depth)
  3. largest_drawdown: The 5 deepest drawdowns, in the same format

Decision Recommendations

  • Max drawdown > -50%: Risk is too high; additional risk controls needed
  • Drawdown duration > 1 year: Strategy may be ineffective long-term
  • Frequent recent drawdowns: Market conditions may have changed; re-evaluate strategy

6. LiquidityAnalysis - Liquidity Risk Assessment

Taiwan market only

LiquidityAnalysis.is_market_supported() returns True only for the Taiwan stock market: it checks Taiwan's daily price limits and its alert, disposal and full-delivery stock rules.

Description

Detects whether the strategy encounters liquidity issues during entry/exit, including:

  • Insufficient volume: Daily trading volume below requirements
  • Insufficient turnover: Daily trading value below requirements
  • Price limits: Buying at upper limit or selling at lower limit (unable to execute)
  • Alert/Disposition/Full-delivery stocks: High-risk securities with trading restrictions

Parameters

class LiquidityAnalysis(
    required_volume=200000,  # Required daily trading volume (default: 200,000 shares)
    required_turnover=1000000,  # Required daily turnover (default: 1,000,000)
)

Usage Example

# Assess the liquidity risk of a Taiwan stock strategy
report.run_analysis('LiquidityAnalysis', required_volume=100000)

Interpreting Output

The table has an entry and an exit row; each column is the share of trades with that risk:

Risk Item Description
buy_high Trades buying at or near the upper price limit
sell_low Trades selling at or near the lower price limit
low_volume_stocks Trades with fewer shares traded than required_volume
low_turnover_stocks Trades with less value traded than required_turnover
Alert / disposition / full-delivery stocks Trades in securities under these Taiwan trading restrictions

Risk threshold suggestions

  • buy_high or sell_low > 10%: Orders may not execute smoothly
  • low_volume_stocks > 20%: Large capital may not fully deploy
  • Disposition stocks > 5%: Strategy may be targeting high-risk securities

Decision Recommendations

  • Insufficient liquidity: Add screening conditions (e.g., "volume > 1,000,000 shares")
  • Frequent price limit hits: Adjust entry/exit timing (e.g., use open price instead of close)
  • Too many alert/disposition stocks: Add risk control conditions to exclude high-risk securities

Custom Analysis Modules

If built-in analysis is insufficient, you can inherit the Analysis class to develop custom analysis.

Basic Example: Market Value Analysis

Average trade return by company size at the entry signal date:

import pandas as pd

from finlab import data
from finlab.analysis import Analysis


class MarketValueAnalysis(Analysis):
    def calculate_trade_info(self, report):
        """Add each trade's market value on its entry signal date"""
        return [['market_value', data.get('etl:us_market_value'), 'entry_sig_date']]

    def analyze(self, report):
        """Average return per market-value quintile"""
        trades = report.get_trades()
        quintile = pd.qcut(trades['market_value@entry_sig_date'], 5, labels=False) + 1
        self.result = trades.groupby(quintile)['return'].agg(['count', 'mean'])
        return self.result.to_dict()

    def display(self):
        """Display analysis results"""
        return self.result


# Use custom analysis
report.run_analysis(MarketValueAnalysis())

Advanced Example: Monthly Win Rate Analysis

Analyze whether win rates differ by month:

import pandas as pd

from finlab.analysis import Analysis


class MonthlyWinRateAnalysis(Analysis):
    def analyze(self, report):
        trades = report.get_trades()
        trades['entry_month'] = pd.to_datetime(trades['entry_date']).dt.month

        monthly_stats = (
            trades.groupby('entry_month')
            .agg({'return': ['count', lambda x: (x > 0).mean(), 'mean']})
            .round(3)
        )

        monthly_stats.columns = ['Trade Count', 'Win Rate', 'Average Return']
        self.result = monthly_stats
        return self.result.to_dict()

    def display(self):
        import plotly.express as px

        fig = px.bar(self.result, y='Win Rate', title='Monthly Win Rate Analysis')
        return fig


# Usage
report.run_analysis(MonthlyWinRateAnalysis())

Analysis Class API

When inheriting the Analysis class, you can override the following methods:

class Analysis:
    def is_market_supported(self, market):
        """Check if the market is supported

        Returns:
            bool: True if supported, False otherwise
        """
        return True

    def calculate_trade_info(self, report):
        """Calculate additional trade information

        Returns:
            list: Format is [[field_name, data DataFrame, date column used], ...]

        Examples:
            return [
                ['market_value', market_value, 'entry_sig_date'],
                ['volume', volume, 'entry_date'],
            ]
        """
        return []

    def analyze(self, report):
        """Execute analysis logic

        Args:
            report: Backtest report object

        Returns:
            dict: Analysis results (for cloud upload)
        """
        pass

    def display(self):
        """Display analysis results

        Returns:
            Visualization object (Plotly Figure or Pandas Styler recommended)
        """
        pass

Purpose of calculate_trade_info

calculate_trade_info() executes before analyze(), automatically adding additional information to the report.get_trades() DataFrame.

For example, returning [['market_value', market_value, 'entry_sig_date']] adds a 'market_value@entry_sig_date' column to trades.


Combining Multiple Analysis Modules

from finlab import data
from finlab.backtest import sim

# Complete backtest
data.set_market('us')
with data.us_universe(index='SP500'):
    close = data.get('price:adj_close')
position = close > close.average(20)
report = sim(position, resample='M')

# 1. Volatility analysis
report.display_mae_mfe_analysis()

# 2. Period stability analysis
report.run_analysis('PeriodStatsAnalysis')

# 3. Alpha/Beta analysis
report.run_analysis('AlphaBetaAnalysis')

# 4. Custom market value analysis
report.run_analysis(MarketValueAnalysis())

Best Practices

1. Standard Analysis Workflow

# Basic performance
report.display()

# Volatility analysis
report.display_mae_mfe_analysis()

# Period stability
report.run_analysis('PeriodStatsAnalysis')

# Alpha / Beta
report.run_analysis('AlphaBetaAnalysis')

# Taiwan stocks only: liquidity risk (check for large capital)
# report.run_analysis('LiquidityAnalysis', required_volume=100000)

2. Analysis Focus by Strategy Type

Strategy Type Key Analysis Module
Short-term trading (intraday, weekly) MaeMfeAnalysis (optimize stop-loss/take-profit)
Long-term investing (monthly, quarterly) PeriodStatsAnalysis (check long-term stability)
Large capital strategies on Taiwan stocks LiquidityAnalysis (avoid liquidity risk)
Market neutral strategies AlphaBetaAnalysis (confirm Beta close to 0)
High-frequency trading DrawdownAnalysis (confirm drawdowns are manageable)

3. Analysis Result Decision Tree

Backtest Complete
  +- Liquidity risk > 20%?
  |   +- Yes -> Add volume screening conditions
  |   +- No  -> Continue
  +- Sharpe ratio < 1?
  |   +- Yes -> Optimize screening conditions or stop-loss/take-profit
  |   +- No  -> Continue
  +- Max drawdown > -30%?
  |   +- Yes -> Add risk controls (stop-loss, position sizing)
  |   +- No  -> Continue
  +- Alpha < 0?
  |   +- Yes -> Strategy has no excess return; needs redesign
  |   +- No  -> Continue
  +- All checks passed -> Proceed to out-of-sample testing

Reference Resources