Training Pools

Why Train Pools?

QuantAMM pools implement trading strategies with configurable parameters that determine how they respond to market conditions. While you can set these parameters manually, finding good values often requires systematic optimization.

Training helps you:

  • Maximize desired metrics (Sharpe ratio, returns, etc.)

  • Adapt strategies to specific market conditions

  • Balance between responsiveness and stability

  • Validate strategy effectiveness

Basic Training Setup

To train a pool, you need to define:

  1. A run fingerprint (configuration) including initial parameters

  2. Training data specifications

Choosing an Optimization Approach

We provide two main approaches to parameter optimization:

Gradient Descent leverages directly JAX’s ability to compute gradients. This is ideal when:

  • You have a good starting point and want to fine-tune

  • The relationship between parameters and performance is smooth

  • You have GPU acceleration

  • You are training in the zero-fees regime (as then there are extremely fast GPU-accelerated algorithms)

Gradient-Free Search works better when:

  • You’re exploring a wide parameter space

  • The performance landscape might be non-smooth (the pool strategy does not have to be differentiable with respect to the strategy parameters)

  • You are training in the presence of fees (as the GPU-accelerated algorithms are not as fast, as fees make modelling AMMs intrinsically sequential)

Parameter Spaces

QuantAMM strategies often use transformed parameters for better optimization. For example:

  • Memory length (\(\text{days}\)) → logit(λ)

    • Ensures λ stays between 0 and 1 by construction

    • Which thus allows unconstrained optimization

    • \(\lambda = \text{sigmoid}(\text{logit_lambda})\)

  • Strategy aggressiveness (k) → log2(k)

    • Handles wide range of scales

    • Maintains positivity

    • \(k = 2^{\text{\log2(k)}}\)

See Constrained vs unconstrained parameters for more examples.

Getting Started

Let’s optimize a QuantAMMmomentum strategy for a BTC/ETH pool:

from quantammsim.runners.jax_runners import train_on_historic_data

# Basic configuration
run_fingerprint = {
    "tokens": ["BTC", "ETH"],
    "rule": "momentum",
    "startDateString": "2024-01-01 00:00:00",
    "endDateString": "2024-03-01 00:00:00",
    "endTestDateString": "2024-04-01 00:00:00",
    "chunk_period": 60,
    "return_val": "daily_log_sharpe",
    "initial_pool_value": 1000000.0
}

Data Handling

The simulator supports flexible data configuration:

  1. Training/Test Split:

    • Specify date ranges for training and testing

    • Automatic data windowing

  2. Data Processing:

    • Minute-level granularity

    • Automatic resampling

    • Missing data handling

    • Price normalization

  3. Training Windows:

    • Fixed or random windows

    • Overlapping periods

    • Custom bout lengths

Gradient Descent Training

For gradient-based optimization:

  1. Set optimization parameters:

run_fingerprint["optimisation_settings"].update({
    "method": "gradient_descent",
    "optimiser": "adam",       # Optimizer type
    "base_lr": 0.01,           # Learning rate
    "batch_size": 16,          # Training batch size
    "n_parameter_sets": 4,     # Number of parameter sets to train in parallel
    "n_iterations": 10000,     # Total iterations
    "decay_lr_plateau": 200,   # Iterations of no improvement before decay
    "decay_lr_ratio": 0.8,     # Learning rate decay on plateau
})
  1. Set off and monitor training:

result = train_on_historic_data(
    run_fingerprint,
    iterations_per_print=100,  # Progress update frequency
    verbose=True               # Detailed logging
)

Gradient-Free Optimization

  1. Configure Optuna settings:

Optuna provides sophisticated optimization without the need for gradients:

run_fingerprint["optimisation_settings"]["optuna_settings"].update({
    "method": "optuna",
    "n_trials": 100,              # Total optimization trials
    "n_jobs": 4,                  # Parallel workers
    "timeout": 7200,              # Max runtime in seconds
    "n_startup_trials": 10,       # Random trials before optimization
    "early_stopping": {
        "enabled": True,
        "patience": 100,          # Trials without improvement
        "min_improvement": 0.001  # Minimum relative improvement
    },
    "parameter_config": {
        "memory_length": {
            "low": 1,
            "high": 200,
            "log_scale": True,    # Search on log scale
            "scalar": False       # Different values per asset
        }
    }
})

The parameter_config supports extensive customization per parameter, including:

  • Search range bounds

  • Linear vs logarithmic scaling

  • Per-asset vs global parameters (see below)

If no parameter_config is provided for a parameter, the simulator will use the default parameter_config.

If a strategy is not differentiable with respect to a parameter, you will have to use this gradient-free optimization approach.

  1. Set off and monitor training:

result = train_on_historic_data(
    run_fingerprint,
    iterations_per_print=100,   # Progress update frequency
    verbose=True               # Detailed logging
)

Parameter & Run Configuration

Pools will automatically initialise as JAX arrays all the parameters that they are set to train. If you wish to set particular initial values for them (for gradient descent training), or the range/sampling method for them (for gradient-free training), you can do so via the run_fingerprint dictionary.

The simulator provides extensive parameter configuration options through the run_fingerprint dictionary. Default values are provided but can be overridden:

    "bout_offset": 24 * 60 * 7,         # Training window offset (in minutes)
    "maximum_change": 3e-4,             # Max weight change per update
    "chunk_period": 1440,               # Strategy update frequency in minutes (1 day)
    "weight_interpolation_period": 1440 # Weight update frequency (1 day)
})

Advanced parameters include:

  • use_alt_lamb: Alternative lambda parameterization so different parts of estimators can have different memory lengths (not supported in QuantAMM V1)

  • use_pre_exp_scaling: Pre-exponential scaling

  • weight_interpolation_method: “linear” or “optimal” (only linear is supported in QuantAMM V1)

  • arb_frequency: Arbitrage check frequency (in minutes)

  • arb_quality: Arbitrage execution efficiency (0-1) (only used for CoW AMM pools)

Bout offset is the number of minutes to offset the training window by. Given how much it can affect the results, it is worth understanding how it works.

Understanding Bout Offset

Bout offset is a crucial parameter that controls how training windows are constructed relative to price data. It specifies an offset in minutes from the duration of the specified training period, in effect shortening the training period by the specified amount.

So if a training run is 4 months long, and the bout offset is 1 month, each window of price data actually used during training will be 3 months long. Those windows can start as early as the first moment of the training period and end as the last moment of the training period.

  1. Configuration

    run_fingerprint.update({
        "bout_offset": 24 * 60 * 7,  # One week in minutes
    })
    
  1. Common Configurations

    • Daily offset: 24 * 60 minutes

    • Weekly offset: 24 * 60 * 7 minutes (default)

    • Custom periods: Any duration

  2. Training Considerations

    • Larger offsets reduce available training data

    • They make the strategy care more about achieving the return metric over the effective (bout-offset reduced) training period

Best Practices:

  • If you want your pool to be more sensitive to partial market conditions, use a longer offset, reducing the length of the windows of data used for training.

  • If you want your pool to optimise more strongly for performance over the exact start date to end date of the training period, use a shorter offset, increasing the length of the windows of data used for training.

Advanced Features

  1. Custom Return Metrics:

run_fingerprint["return_val"] = "daily_log_sharpe"  # or "sharpe", "returns", "sortino" among others
  1. Multi-period Training (Gradient-free only):

run_fingerprint.update({
    "optimisation_settings": {
        "method": "optuna",
        "optuna_settings": {
            "multi_objective": True,
        }
    }
})

This sets the optimisation to be multi-objective, and will optimise the chosen return metric for a range of different periods within the training data, a mixture of sequenital, periodically placed windows and randomly placed windows (to avoid periodic biases). This is useful for finding a good set of parameters that perform well across a range of market conditions. The mean and standard deviation of the return metric across the different periods is returned in the result, along with the worst value.

Note

This is only supported in the gradient-free optimisation approac, but gradient-based optimisation naturally has this property as each window of data used for training is a) reduced in length by the bout offset and b) randomly placed.

  1. Constrain a parameter to be “universal” (i.e. the same value for all assets) (Gradient-free only):

run_fingerprint["optimisation_settings"]["optuna_settings"]["parameter_config"]["k_per_day"]["scalar"] = True

This is set on a per-parameter basis, and will force the parameter to be the same for all assets. Constraining parameters in this way can help with generalisation.

  1. Set initial parameter values (Gradient-descent only):

run_fingerprint.update({
    "initial_memory_length": 10.0,    # Starting memory length in days
    "initial_k_per_day": 20,          # Starting strategy aggressiveness
})

Performance Considerations

  • Use GPU acceleration when available

  • Batch size, n_parameter_sets (for gradient descent) and n_jobs (for gradient-free) affect memory usage

  • Monitor for overfitting using test period and other post-run analysis

See Also