Skip to content

Strategy Parameter Optimization

After developing a trading strategy, how do you find the best combination of conditions? The finlab.optimize module provides the sim_conditions() function to automatically test all condition combinations and visually compare performance, helping you identify the strongest strategy.

Why Strategy Optimization?

When developing trading strategies, we often have multiple screening conditions:

  • Technical: Moving average crossovers, new highs, RSI indicators
  • Fundamental: P/E ratio, revenue growth, profitability
  • Institutional: Institutional buying, margin balance changes

Each condition alone may produce mediocre performance, but combining multiple conditions may yield unexpected results. Manually testing all combinations is extremely time-consuming; sim_conditions() automates this process.

Quick Start

Basic Example: All Combinations of 3 Conditions

Suppose we have 3 screening conditions:

from finlab import data
from finlab.backtest import sim
from finlab.optimize.combinations import sim_conditions

close = data.get("price:收盤價")
rev = data.get('monthly_revenue:當月營收')
營業利益成長率 = data.get('fundamental_features:營業利益成長率')

# Define 3 conditions
c1 = (close > close.average(20)) & (close > close.average(60))  # Technical: MA breakout
c2 = 營業利益成長率 > 0                                          # Fundamental: profit growth
c3 = rev.average(3) / rev.average(12) > 1.1                     # Fundamental: revenue acceleration

# Set exit condition
exits = close < close.average(20)

# Conditions dictionary, keys are condition names
conditions = {'c1': c1, 'c2': c2, 'c3': c3}

# Test all combinations
report_collection = sim_conditions(
    conditions=conditions,
    hold_until={'exit': exits, 'stop_loss': 0.1},  # Exit when below MA, 10% stop-loss
    resample='M',           # Monthly rebalancing
    position_limit=0.1,     # Max 10% per stock
    upload=False            # Don't upload to cloud
)

This code automatically tests the following 7 combinations:

  1. c1 (technical only)
  2. c2 (profit growth only)
  3. c3 (revenue acceleration only)
  4. c1 & c2 (technical + profit growth)
  5. c1 & c3 (technical + revenue acceleration)
  6. c2 & c3 (profit growth + revenue acceleration)
  7. c1 & c2 & c3 (all three conditions)

Visual Performance Comparison

1. Cumulative Return Line Chart

Compare the equity curves of each combination:

report_collection.plot_creturns().show()

Cumulative Returns

From the chart, you can see which combination has the best long-term performance and the stability of each combination.

2. Grouped Bar Chart of Metrics

report_collection.plot_stats('bar').show()

Bar Chart

Displays 12 key metrics side by side for all combinations for quick comparison.

3. Metric Ranking Heatmap

report_collection.plot_stats('heatmap')

Heatmap

The heatmap uses color intensity to represent the percentile ranking (0-100%) of each metric. Higher values indicate better rankings:

  • avg_score: Average score across all metrics; higher scores indicate better overall performance
  • Sorted by avg_score in descending order by default, with the best combination at the top
  • Brighter colors (yellow) indicate higher rankings for that metric

How to interpret the heatmap

  • Look for the combination with the highest avg_score (usually at the top)
  • Check that the combination performs well on key metrics (Sharpe ratio, win rate, max drawdown)
  • Avoid combinations with especially poor metrics (dark purple)

Get Detailed Performance Metrics

stats = report_collection.get_stats()
print(stats)

Returns a DataFrame with 12 metrics:

Strategy-Level Metrics

Metric Description Direction
daily_mean Annualized return Higher is better
daily_sharpe Annualized Sharpe ratio (risk-adjusted return) Higher is better
daily_sortino Annualized Sortino ratio (downside risk-adjusted return) Higher is better
max_drawdown Maximum drawdown (negative value) Smaller absolute value is better
avg_drawdown Average drawdown (negative value) Smaller absolute value is better

Trade-Level Metrics

Metric Description Direction
win_ratio Win rate per trade Higher is better
avg_return Average profit per trade Higher is better
avg_mae Average MAE per trade (negative value) Smaller absolute value is better
avg_bmfe Average BMFE (MFE before MAE) per trade Higher is better
avg_gmfe Average global MFE per trade Higher is better
avg_mdd Average max drawdown per trade (negative value) Smaller absolute value is better

Meaning of avg_bmfe

avg_bmfe represents how high the stock price went before the stop-loss was triggered. The higher this value, the more opportunity there is to take profit before the stop-loss -- an important reference for optimizing take-profit points.

Advanced Example: Optimization with 5 Conditions

When conditions increase to 5, the number of combinations reaches 31 (2^5 - 1). Manual testing is very difficult, but sim_conditions() handles it easily:

from finlab import data
from finlab.optimize.combinations import sim_conditions

close = data.get("price:收盤價")
pe = data.get('price_earning_ratio:本益比')
rev = data.get('monthly_revenue:當月營收').index_str_to_date()
rev_ma3 = rev.average(3)
rev_ma12 = rev.average(12)

# 5 conditions
c1 = (close > close.average(20)) & (close > close.average(60))  # MA bullish alignment
c2 = (close == close.rolling(20).max())                         # 20-day new high
c3 = pe < 15                                                    # Low P/E
c4 = rev_ma3 / rev_ma12 > 1.1                                   # Revenue acceleration
c5 = rev / rev.shift(1) > 0.9                                   # Monthly revenue change > -10%

exits = close < close.average(20)

conditions = {'c1': c1, 'c2': c2, 'c3': c3, 'c4': c4, 'c5': c5}

report_collection = sim_conditions(
    conditions=conditions,
    hold_until={'exit': exits, 'stop_loss': 0.1},
    resample='M',
    position_limit=0.1,
    upload=False
)

# View heatmap to quickly find the best combination
report_collection.plot_stats('heatmap')

Interpreting Results

Suppose the heatmap shows c1 & c3 & c4 has the highest avg_score:

  1. Check cumulative return curve: Confirm steady NAV growth
  2. Check key metrics:
  3. Sharpe ratio > 1.5? (Is risk-adjusted return sufficient?)
  4. Max drawdown < -30%? (Can you tolerate the worst case?)
  5. Win rate > 50%? (Is the trade win rate reasonable?)
  6. Analyze combination meaning: c1 & c3 & c4 represents "technical breakout + cheap valuation + revenue growth" -- this combination has clear business logic

Customize Displayed Metrics

If you only care about specific metrics, use the indicators parameter:

# Show only return, Sharpe ratio, and max drawdown
report_collection.plot_stats('bar', indicators=['daily_mean', 'daily_sharpe', 'max_drawdown']).show()

# Sort heatmap by Sharpe ratio
report_collection.plot_stats('heatmap', heatmap_sort_by='daily_sharpe')

# Sort heatmap by multiple metrics
report_collection.plot_stats('heatmap', heatmap_sort_by=['daily_sharpe', 'win_ratio'])

Combining with Stop-Loss/Take-Profit Optimization

The hold_until parameter of sim_conditions() supports various exit logics:

# 1. Exit signal only
hold_until = {'exit': exits}

# 2. Exit signal + stop-loss
hold_until = {'exit': exits, 'stop_loss': 0.1}

# 3. Exit signal + take-profit
hold_until = {'exit': exits, 'take_profit': 0.2}

# 4. Exit signal + stop-loss + take-profit
hold_until = {'exit': exits, 'stop_loss': 0.1, 'take_profit': 0.2}

# 5. Exit signal + stop-loss (trailing stop requires trail_stop in sim())
hold_until = {'exit': exits, 'stop_loss': 0.1}
# Combined with: sim_conditions(..., trail_stop=0.15)

Stop-loss/take-profit setting suggestions

  1. First run report.display_mae_mfe_analysis() to analyze volatility characteristics
  2. Set the stop-loss point based on the MAE distribution (avoid over-stopping)
  3. Set the take-profit point based on the MFE distribution (ensure profits are realized)
  4. Use sim_conditions() to test different stop-loss/take-profit combinations

FAQ and Best Practices

Q1: Too many combinations causing long computation time?

When the number of conditions > 6, combinations exceed 63. Suggestions:

  1. Pre-filter important conditions: Use single-condition backtests to remove poorly performing conditions
  2. Test in batches: Separate conditions into technical, fundamental, and institutional groups
  3. Use longer resample: Switch to resample='Q' (quarterly) to reduce computation

Q2: How to avoid overfitting?

  1. Out-of-sample testing: Optimize on historical data, validate on recent data
  2. Avoid too many conditions: Be especially careful when conditions > 5
  3. Check combination logic: Does the best combination have business logic support?
  4. Evaluate multiple metrics: Don't just look at returns; also consider Sharpe ratio, drawdown, and win rate

Q3: What if conditions have inconsistent data frequencies?

FinLab automatically aligns data frequencies:

# Daily data
close = data.get("price:收盤價")  # Updated daily

# Monthly data
rev = data.get('monthly_revenue:當月營收')  # Updated monthly

# Quarterly data
eps = data.get('financial_statement:每股盈餘')  # Updated quarterly

# Mixing frequencies works fine; FinLab auto forward-fills
conditions = {
    'c1': close > close.average(20),
    'c2': rev.average(3) > rev.average(12),
    'c3': eps > 0
}

Q4: Why do some combinations fail backtesting?

Possible causes:

  1. Conditions too strict: No stocks meet the intersection criteria
  2. Missing data: Some conditions have insufficient data coverage
  3. Insufficient memory: Too many combinations cause OOM

Check the log for specific error messages.

Q5: How to access a specific combination's backtest report?

# report_collection.reports is a dict
print(report_collection.reports.keys())
# Output: dict_keys(['c1', 'c2', 'c3', 'c1 & c2', 'c1 & c3', ...])

# Get a specific combination's report
report = report_collection.reports['c1 & c3']
report.display()

Practical Workflow Recommendations

  1. Define candidate conditions (5-8)
  2. Single-condition backtest: Confirm each condition has basic performance
  3. Use sim_conditions(): Test all combinations
  4. Visual analysis:
  5. Use heatmap to find the top 3 combinations
  6. Use line charts to confirm NAV stability
  7. Use bar charts to compare key metrics
  8. Deep dive into top 3:
  9. Run MAE/MFE analysis
  10. Check liquidity risk
  11. Run out-of-sample tests
  12. Select final strategy: Consider performance, risk, and logical soundness

Reference Resources