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
- Win Ratio: Trade win rate distribution (profit vs loss histogram)
- Blue: profitable trades, Pink: losing trades
-
Green dashed line: average return
-
Edge Ratio: MFE/MAE ratio at different holding periods
- Value > 1 means profit magnitude exceeds loss magnitude
-
Observe the strategy's "edge time window"
-
MAE vs Return: Relationship between MAE and final return
- Horizontal dashed line: MAE 75th percentile (Q3)
- Used for setting stop-loss points
Row 2
- GMFE vs MAE: Maximum profit vs maximum loss
- Q3 dashed lines: Range of 75% of trades' GMFE and MAE
-
Used for evaluating take-profit potential
-
BMFE vs MAE: Maximum profit before MAE occurred
- Higher values mean more opportunity to take profit before stop-loss triggers
-
Used for optimizing "trailing stop" strategies
-
MDD vs GMFE: Maximum drawdown vs maximum profit
- Orange line: MDD = GMFE baseline
- Points below: GMFE > MDD (good signal, trailing stop viable)
- 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
- Indices Stats (Violin Plot): Statistical distribution of 6 key metrics
- return, mae, bmfe, gmfe, mdd, pdays_ratio
- group mode: Shows profit/loss trade differences separately (recommended)
- 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
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
Interpreting Output
Shows three sections:
-
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%) -
Yearly Alpha/Beta -- Annual Alpha and Beta breakdown to observe strategy stability
-
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
Interpreting Output
The result is a dict with a strategy and a benchmark entry, each containing:
drawdown: The drawdown series (indexandvalues)longest_drawdown: The 5 longest drawdowns, keyed by start date, withEnd,Length(days) anddrawdown(depth)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_highorsell_low> 10%: Orders may not execute smoothlylow_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