Part 2A;Maximizing Returns: Data-Driven Optimization of Trading Strategies with Optuna.

ogunniran sijiogunniran siji
7 min read

Trend-following strategies have long been a staple in the world of trading, offering traders the opportunity to profit from sustained price movements in financial markets. However, the effectiveness of these strategies often hinges on the careful selection of parameters and rules. In this article, we'll explore how you can harness the power of Optuna, a versatile Python library for hyperparameter optimization, to optimize your trend-following trading strategies.

The Importance of Optimization

Trend-following strategies aim to capture price trends by buying or selling assets based on specific conditions or indicators. These strategies can be highly profitable when properly configured, but finding the optimal parameters can be a complex and time-consuming task. This is where Optuna comes into play.

Optuna is not just for hyperparameter optimization in machine learning; it can also be applied to optimize trading strategies. By using Optuna, you can systematically search for the best combination of parameters, which can significantly improve your strategy's performance and profitability.

Getting Started

Before diving into the details of optimizing trend-following strategies, let's set up the environment and clarify the objectives.

Prerequisites

To follow along with this optimization process, you'll need the following prerequisites:

  • Python is installed on your system.

  • Knowledge of trend-following strategies and trading concepts.

  • Familiarity with Optuna, so you can navigate its functionality effectively.

Setting Up the Environment

To optimize your trend-following strategy using Optuna, we'll utilize a set of Python scripts and libraries. The following Python script is used to configure the optimization process:

# Import necessary libraries and modules
import datetime
import argparse
import optuna
import pandas as pd

# Define date format and retrieve today's date
DATE_FORMAT = '%Y-%m-%d'
today = datetime.datetime.now()

# Define command-line arguments
ap = argparse.ArgumentParser(description="Run optuna optimization on an existing study")

ap.add_argument('--study-name', '-s', type=str, required=True, help="existing study name")
ap.add_argument('--algo', '-a', type=str, required=True, help="algo name to optimize for")
ap.add_argument('--from', '-f', dest="from_date", type=str, required=True, help="from date")
ap.add_argument('--to', '-t', dest="to_date", type=str, default=today.strftime(DATE_FORMAT), help="to date")
ap.add_argument('--funds', type=int, default=1e6, help="funds")
ap.add_argument('--trials', type=int, default=5, help="number of trials to run")
ap.add_argument('--warmup', '-w', type=int, default=270, help="warmup")
ap.add_argument('--tags', default="st-opt", help="tags for optimization runs")
ap.add_argument('--db', default="postgresql://localhost:5432/optuna", help="db uri")

args = ap.parse_args()

# Define a function for generating strategy statistics
def generate_backtest_stats(algo, start_date, end_date, symbols, params, trade_percent=1, funds=1e6, max_contracts=1000):

    return [ret, skewness]

# Define the objective class
class Objective:
    def __init__(self, start_date, end_date, symbols, algo, funds):
        self.start = start_date
        self.end = end_date
        self.symbols = symbols
        self.algo = algo
        self.funds = funds

    def __call__(self, trial):
        # Define strategy parameters to optimize
        params = {
            'candle_length': trial.suggest_categorical('candle_length', [30 * 60 * 1000, 60 * 60 * 1000, 120 * 60 * 1000, 240 * 60 * 1000]),
            'use_dynamic_brackets': trial.suggest_categorical('use_dynamic_brackets', [0,1]),
            'use_stdev_brackets': trial.suggest_categorical('use_stdev_brackets', [0, 1]),
            'update_brackets': trial.suggest_categorical('update_brackets', [0, 1]),
            'reenter_days': trial.suggest_int('reenter_days', 1, 30),
            'stop': trial.suggest_float('stop', 0.005, 0.1, step=0.003), 
            'target':  trial.suggest_float('target', 0.01, 0.3, step=0.005),
            'trailing_target': trial.suggest_int('trailing_target', 0, 20),
            'std_window': trial.suggest_float('std_window', 10, 100, step=10),
        }

        if self.algo == 'rsi_alloc':
            params['short'] = trial.suggest_categorical('short', [0, 1])
            params['rsi_len'] = trial.suggest_int('rsi_len', 10, 50, step=5)
            params['top'] = trial.suggest_int('top', 60, 95, step=5)
            params['bottom'] = trial.suggest_int('bottom', 5, 40, step=5)

        if self.algo == 'week52_alloc':
            params['short'] = trial.suggest_categorical('short', [0, 1])
            params['tq'] = trial.suggest_categorical('tq', [0, 1])
            params['week_len'] = trial.suggest_categorical('week_len', [24, 52])

        if params['use_stdev_brackets'] == 1 or params['use_dynamic_brackets'] == 1:
            if params['trailing_target'] > 0:
                params['skip_reverse_position'] = trial.suggest_categorical('skip_reverse_position', [0, 1])

        if params['use_stdev_brackets'] == 1:
            params['stop_mult'] = trial.suggest_float('stop_mult', 0.1, 1.5, step=0.1)
            params['target_mult'] = trial.suggest_float('target_mult', 0.1, 1.5, step=0.1)

        if params['use_dynamic_brackets'] == 1:
            params['use_stdev_brackets'] = 0
            params['quantile'] = trial.suggest_float("quantile", 0.3, 0.9, step=0.05)
            params['winner_quantile'] = trial.suggest_float("winner_quantile", 0.5, 1.0, step=0.05)
            params['use_winners'] = trial.suggest_categorical('use_winners', [1])
            params['samples_count'] = trial.suggest_int('samples_count', 15, 30, step=5)

        return generate_stats(self.algo, self.start, self.end, symbols=self.symbols, funds=self.funds, params=params)

# Define the list of symbols to optimize
symbols = [
    # List of symbols to optimize for
]

# Main optimization process
if __name__ == '__main__':
    from_date = args.from_date
    to_date = args.to_date

    algo = args.algo
    n_trials = args.trials

    # Load the existing Optuna study
    study = optuna.load_study(
        study_name=args.study_name, storage=args.db
    )

    objective = Objective(from_date, to_date, symbols, algo, args.funds)
    study.optimize(objective, n_trials=n_trials)

    print(f"Best params: {study.best_trials[:3]}")

Lets breakdown this code to the following steps:

Understanding the Optimization Process

Optimizing a trend-following strategy involves finding the right combination of parameters that yield the highest returns while managing risks effectively. To achieve this, we'll explore the key elements of the optimization process:

1. Selecting Relevant Parameters

  •               # Define strategy parameters to optimize
                  params = {
                      'candle_length': trial.suggest_categorical('candle_length', [30 * 60 * 1000, 60 * 60 * 1000, 120 * 60 * 1000, 240 * 60 * 1000]),
                      'use_dynamic_brackets': trial.suggest_categorical('use_dynamic_brackets', [0,1]),
                      'use_stdev_brackets': trial.suggest_categorical('use_stdev_brackets', [0, 1]),
                      'update_brackets': trial.suggest_categorical('update_brackets', [0, 1]),
                      'reenter_days': trial.suggest_int('reenter_days', 1, 30),
                      'stop': trial.suggest_float('stop', 0.005, 0.1, step=0.003), 
                      'target':  trial.suggest_float('target', 0.01, 0.3, step=0.005),
                      'trailing_target': trial.suggest_int('trailing_target', 0, 20),
                      'std_window': trial.suggest_float('std_window', 10, 100, step=10),
                  }
    

Optuna allows us to optimize various parameters of a trend-following strategy. These parameters include:

  • Candle Length: The duration of each candle or time interval used for analysis (e.g., 30 minutes, 1 hour, etc.).

  • Dynamic vs. Static Brackets: Choosing whether to use dynamic brackets (adjusting stop and target levels based on market conditions) or static brackets (fixed stop and target levels).

  • Reentry Days: The number of days to wait before reentering a trade after an exit.

  • Stop and Target Levels: The price levels at which to set stop-loss and take-profit orders.

  • Trailing Target: Enabling or disabling a trailing stop for maximizing profits.

  • Standard Deviation Window: The window size for calculating standard deviations in dynamic bracket strategies.

2. Defining Trading Strategies

        if self.algo == 'rsi_alloc':
            params['short'] = trial.suggest_categorical('short', [0, 1])
            params['rsi_len'] = trial.suggest_int('rsi_len', 10, 50, step=5)
            params['top'] = trial.suggest_int('top', 60, 95, step=5)
            params['bottom'] = trial.suggest_int('bottom', 5, 40, step=5)

        if self.algo == 'week52_alloc':
            params['short'] = trial.suggest_categorical('short', [0, 1])
            params['tq'] = trial.suggest_categorical('tq', [0, 1])
            params['week_len'] = trial.suggest_categorical('week_len', [24, 52])

        if params['use_stdev_brackets'] == 1 or params['use_dynamic_brackets'] == 1:
            if params['trailing_target'] > 0:
                params['skip_reverse_position'] = trial.suggest_categorical('skip_reverse_position', [0, 1])

        if params['use_stdev_brackets'] == 1:
            params['stop_mult'] = trial.suggest_float('stop_mult', 0.1, 1.5, step=0.1)
            params['target_mult'] = trial.suggest_float('target_mult', 0.1, 1.5, step=0.1)

        if params['use_dynamic_brackets'] == 1:
            params['use_stdev_brackets'] = 0
            params['quantile'] = trial.suggest_float("quantile", 0.3, 0.9, step=0.05)
            params['winner_quantile'] = trial.suggest_float("winner_quantile", 0.5, 1.0, step=0.05)
            params['use_winners'] = trial.suggest_categorical('use_winners', [1])
            params['samples_count'] = trial.suggest_int('samples_count', 15, 30, step=5)

In this optimization process, we'll focus on different trend-following strategies, such as:

  • Supertrend: A popular trend-following indicator that uses volatility to determine stop and target levels.

  • RSI Allocation: Incorporating the Relative Strength Index (RSI) to identify overbought and oversold conditions.

  • Week 52 Allocation: Adapt strategies based on the time of year, such as 52-week highs and lows.

Each strategy has its unique parameters and characteristics, making it crucial to optimize them individually.

3. Optimizing Risk and Reward

The optimization process aims to balance risk and reward. We seek to maximize returns while minimizing potential losses. This involves fine-tuning parameters like stop and target levels and dynamically adjusting them based on market conditions.

4. Evaluating Performance Metrics

To determine the effectiveness of each optimized strategy, we'll evaluate several performance metrics:

  • Return on Investment (ROI): The primary metric for measuring profitability. We aim to maximize ROI while considering other metrics.

  • Sortino Ratio: Measures the risk-adjusted return, emphasizing downside volatility.

  • Maximum Drawdown: The peak-to-trough decline during a specific period, indicating the strategy's risk.

  • Number of Trades: Reflects the frequency of trading activity.

  • Skewness: Measures the asymmetry of returns distribution, helping assess the strategy's risk.

  • Maximum Leverage: Evaluates the maximum leverage used in the strategy.

# Define a function for generating strategy statistics
def generate_backtest_stats(algo, start_date, end_date, symbols, params, trade_percent=1, funds=1e6, max_contracts=1000):

    return [ret, skewness]

Conclusion

In this part of our series, we've explored the core elements of the optimization process for trend-following strategies. We've learned how to select relevant parameters, define trading strategies, optimize risk and reward, and evaluate performance metrics.

In the next installment, we'll dive into practical examples, showcasing how to set up Optuna for strategy optimization and interpret the results. Stay tuned for hands-on guidance on fine-tuning your trend-following strategies for maximum returns.

Conclusion

In this first part of our series on optimizing trend-following trading strategies with Optuna, we've introduced the concept of optimization, set up the necessary environment, and explored the Python script that forms the foundation of our optimization process.

Check out Part 2B to learn how you can finetune any strategy with Optuna

In the next installment, we'll delve deeper into the optimization process itself, analyzing the various parameters, strategies, and metrics used to maximize returns. Stay tuned for more insights into how Optuna can help you refine your trading strategies and enhance your profitability.

1
Subscribe to my newsletter

Read articles from ogunniran siji directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

ogunniran siji
ogunniran siji

I am a Machine Learning and Quant developer, Having worked around building Machine Learning projects and Optimizing returns