Run Fingerprints
This guide explains how to configure runs using the run_fingerprint dictionary of settings that is used by high-level simulation runners.
Basic Settings
Core simulation configuration:
run_fingerprint = {
"startDateString": "2024-02-03 00:00:00", # Simulation start date
"endDateString": "2024-06-03 00:00:00", # Training end / simulation end date
"endTestDateString": "2024-07-03 00:00:00", # Test period end (optional)
"tokens": ["BTC", "ETH", "USDC"], # Assets to simulate
"rule": "momentum", # Strategy/pool type
"initial_pool_value": 1000000.0, # Starting pool value in USD
}
Available Pool Rules
The rule parameter accepts the following values:
Static Weight Pools:
balancer- Standard Balancer-style constant-weight poolhodl- Simple hold strategy (no rebalancing)cow- CoW AMM poolgyroscope- Gyroscope ECLP pool
Dynamic Weight Pools (QuantAMM):
momentum- Trend-following strategyanti_momentum- Counter-trend strategymean_reversion_channel- Mean reversion with channel breakouttriple_threat_mean_reversion_channel- Channel mean reversion + trend + interactionpower_channel- Power-law weighted momentumdifference_momentum- Differential momentum strategymin_variance- Minimum variance portfolioindex_market_cap- Market-cap weighted indexhodling_index_market_cap- Index pool with on-chain HODLing behaviourtrad_hodling_index_market_cap- Index pool with CEX trading costs
Hooked Pools:
Add hooks using the hookname__poolrule prefix format:
lvr__momentum- Loss-Versus-Rebalancing trackingrvr__balancer- Rebalancing-Versus-Rebalancing trackingbounded__momentum- Per-asset weight boundsensemble__momentum- Ensemble averaging over multiple parameter sets
Hooks can be chained with multiple double-underscore prefixes:
# Ensemble + bounded weights + mean reversion channel
pool = create_pool("ensemble__bounded__mean_reversion_channel")
See Pool Hooks for details on hooks and Per-Asset Weight Bounds for bounded weight pools.
Optimization Settings for Gradient Descent
Control the training process:
run_fingerprint["optimisation_settings"] = {
"method": "gradient_descent",
"base_lr": 0.1, # Initial learning rate
"optimiser": "adam", # Optimizer: "adam", "adamw", or "sgd"
"batch_size": 8, # Training batch size
"n_iterations": 1000, # Training iterations
"n_parameter_sets": 4, # Parallel parameter sets
"training_data_kind": "historic", # Data source type
}
Available Optimizers
adam- Adam optimizer (recommended for most cases)adamw- Adam with weight decaysgd- Stochastic gradient descent
Learning Rate Scheduling
run_fingerprint["optimisation_settings"].update({
"lr_schedule_type": "constant", # "constant" or "warmup_cosine"
"warmup_steps": 100, # Warmup steps for cosine schedule
"min_lr": 1e-6, # Minimum learning rate
"use_plateau_decay": False, # Decay LR on plateau
"decay_lr_plateau": 100, # Iterations before decay
"decay_lr_ratio": 0.8, # Decay multiplier
})
Gradient Clipping
run_fingerprint["optimisation_settings"].update({
"use_gradient_clipping": True, # Enable gradient clipping
"clip_norm": 10.0, # Maximum gradient norm
})
Initial Parameters
Starting values for strategy parameters, used only during training.
run_fingerprint.update({
"initial_memory_length": 10.0, # Memory parameter
"initial_k_per_day": 20, # Trading intensity
"initial_weights_logits": 1.0, # Starting weights. Provide a jnp.array of length = num of tokens for per-token allocation, otherwise defaults to uniform weights.
"initial_log_amplitude": -10.0, # Signal amplitude
})
Optimization Settings for Gradient-Free Descent
For hyperparameter optimization using Optuna:
run_fingerprint["optimisation_settings"]["method"] = "optuna"
run_fingerprint["optimisation_settings"]["optuna_settings"] = {
"n_trials": 20, # Number of trials
"n_jobs": 4, # Parallel workers
"timeout": 7200, # Max runtime (seconds)
"parameter_config": {
"memory_length": {
"low": 1, # Min value
"high": 200, # Max value
"log_scale": True, # Use log scale
},
# ... other parameters ...
}
}
Complete Optuna Settings Reference
optuna_settings = {
# Study configuration
"study_name": None, # Auto-generated if None
"storage": {
"type": "sqlite", # "sqlite", "mysql", or "postgresql"
"url": None, # e.g., "sqlite:///studies.db"
},
# Trial settings
"n_trials": 20, # Number of optimization trials
"n_jobs": 4, # Parallel workers
"timeout": 7200, # Max optimization time (seconds)
"n_startup_trials": 10, # Random trials before TPE sampler
# Early stopping
"early_stopping": {
"enabled": False,
"patience": 100, # Trials without improvement
"min_improvement": 0.001, # Minimum relative improvement
},
# Search behavior
"expand_around": True, # Search around initial values (see below)
"multi_objective": False, # Multi-objective optimization
"make_scalar": False, # Force scalar objective
# Overfitting control
"overfitting_penalty": 0.0, # Penalize train >> validation (see below)
# Parameter search ranges
"parameter_config": { ... }
}
Search Behavior: expand_around
The expand_around setting controls how parameter ranges are interpreted:
expand_around: True- Search within a window around initial parameter values. Good for fine-tuning when you have reasonable starting points.expand_around: False- Search the full range specified inparameter_config. Better for exploration when optimal values are unknown.
For financial strategies, False often gives better exploration of the parameter space.
Overfitting Penalty
The overfitting_penalty discourages solutions where training performance greatly exceeds validation:
# Penalty calculation:
# penalty = overfitting_penalty * max(0, train_score - validation_score)
#
# Example: train=1.0, val=0.5, penalty_weight=0.5
# penalty = 0.5 * (1.0 - 0.5) = 0.25
# adjusted_score = validation_score - penalty = 0.5 - 0.25 = 0.25
run_fingerprint["optimisation_settings"]["optuna_settings"]["overfitting_penalty"] = 0.3
Set to 0.0 to disable. Range [0.0, 1.0] recommended.
Parameter Configuration
Each parameter in parameter_config accepts:
"parameter_name": {
"low": 1, # Minimum value
"high": 200, # Maximum value
"log_scale": True, # Use logarithmic scale
"scalar": False, # Same value for all assets (True) or per-asset (False)
}
Available parameters:
Parameter |
Default Range |
Log Scale |
Description |
|---|---|---|---|
|
1-200 |
Yes |
EWMA memory in days |
|
0.1-100 |
Yes |
Memory length variation |
|
-10 to 10 |
No |
Log trading intensity |
|
0.1-1000 |
Yes |
Trading intensity per day |
|
-10 to 10 |
No |
Initial weight logits |
|
-10 to 10 |
No |
Signal amplitude (log scale) |
|
-10 to 10 |
No |
Channel width |
|
0-10 |
No |
Power exponents |
|
-10 to 10 |
No |
Pre-exponential scaling |
|
-10 to 10 |
No |
Logit-transformed lambda |
For more details on Optuna optimization see Training Pools.
Return Metrics
The return_val parameter determines the objective function for training:
run_fingerprint["return_val"] = "daily_log_sharpe" # Default
Common metrics:
daily_log_sharpe- Daily log-return Sharpe ratio (default)sharpe- Annualised Sharpe ratiodaily_sharpe- Daily Sharpe ratioreturns- Total return over simulation periodreturns_over_hodl- Return relative to holding the initial portfolioreturns_over_uniform_hodl- Return relative to uniform hold of all assetscalmar- Calmar ratio (return / max drawdown)sterling- Sterling ratio (return / average drawdown)greatest_draw_down- Maximum drawdown from initial valueweekly_max_drawdown- Worst drawdown across weekly chunksdaily_var_95%- 5th percentile of daily returns (VaR)daily_raroc- Risk-Adjusted Return on Capital (daily)daily_rovar- Return on VaR (daily)ulcer- Ulcer Index (measures drawdown duration and depth)
See Metrics Reference for the full list of ~30 available metrics.
Runtime Behavior
Configure execution details:
run_fingerprint.update({
"maximum_change": 3e-4, # Max weight change per update
"chunk_period": 1440, # Strategy update frequency (minutes)
"weight_interpolation_period": 1440, # Weight change frequency (minutes)
"weight_interpolation_method": "linear", # "linear" or "optimal"
"minimum_weight": None, # Min weight per asset (default: 0.1/n_assets)
"max_memory_days": 365, # Maximum lookback for estimators
"weight_calculation_method": "auto", # Weight calculation path (see below)
})
Weight Calculation Method
The weight_calculation_method parameter controls how pool weights are computed:
auto(default) - Automatically selects the best available path for the poolvectorized- Uses vectorized convolution-based computation (faster for most pools)scan- Uses sequential scan-based computation (mirrors production execution)
# Force scan-based computation (matches on-chain execution)
run_fingerprint["weight_calculation_method"] = "scan"
# Force vectorized computation (typically faster)
run_fingerprint["weight_calculation_method"] = "vectorized"
When to use each:
Use
autofor most cases - it selects vectorized when availableUse
scanwhen you need to verify results match production/on-chain behaviorUse
vectorizedexplicitly if you want to ensure the faster path is used
Pool support:
Most QuantAMM pools (momentum, power_channel, mean_reversion_channel) support both paths and produce numerically equivalent results. Some pools (e.g., min_variance) only support the vectorized path.
See Weight Calculation Paths for detailed information about the two computation paths.
Fee and Arbitrage Settings
run_fingerprint.update({
"fees": 0.003, # Trading fees (e.g., 30bps)
"arb_fees": 0.0, # Fees paid by arbitrageurs
"gas_cost": 0.0, # Gas cost per arbitrage trade
"do_arb": True, # Enable arbitrage simulation
"arb_frequency": 1, # Arb check frequency (minutes)
"arb_quality": 1.0, # Arbitrage efficiency (0-1)
})
Advanced Settings
Straight-Through Estimators
For improved gradient flow during training through non-differentiable clipping operations:
run_fingerprint.update({
"ste_max_change": False, # STE for max weight change clipping
"ste_min_max_weight": False, # STE for min/max weight bounds
})
When True, these allow gradients to flow through clipping operations during backpropagation,
which can improve training stability and convergence.
Alternative Lambda Parameterization
run_fingerprint.update({
"use_alt_lamb": False, # Per-estimator memory lengths
"use_pre_exp_scaling": True, # Pre-exponential scaling
})
use_alt_lamb- WhenTrue, allows different memory lengths for different estimatorsuse_pre_exp_scaling- WhenTrue, applies pre-exponential scaling to weight changes
Noise Traders
run_fingerprint.update({
"noise_trader_ratio": 0.0, # Ratio of noise trader volume (0-1)
})
Simulates uninformed trading activity. Value of 0.1 means 10% of volume comes from noise traders.
Numeraire Token
run_fingerprint.update({
"numeraire": None, # Token used as price base (default: last token)
})
When None, the last token in the tokens list is used as the numeraire.
Set explicitly if you want prices quoted in a specific token.
Timing Parameters
Understanding the relationship between timing parameters:
run_fingerprint.update({
"chunk_period": 1440, # Strategy evaluation frequency (minutes)
"weight_interpolation_period": 1440, # Weight update frequency (minutes)
"bout_offset": 24 * 60 * 7, # Temporal sampling range (minutes)
})
chunk_period- How often the strategy calculates new target weights. 1440 = daily, 60 = hourly, 1 = per minute.weight_interpolation_period- How often weights actually change. Must be <= chunk_period. When < chunk_period, weights are interpolated between chunk evaluations.bout_offset- Temporal sampling range in minutes. See bout_offset (Temporal Sampling Range) below.
bout_offset (Temporal Sampling Range)
bout_offset is the number of minutes subtracted from the total training
window to define the length of each sampled forward pass. The remainder
becomes the range of possible start positions, giving different batches
different temporal views of the data.
bout_length_window = effective_train_length - bout_offset
Each training iteration, a start position is randomly drawn from the first
bout_offset minutes of the training region, and the forward pass runs for
exactly bout_length_window steps from that position. All sampled windows
are the same length; only their start positions differ.
Default: 24 * 60 * 7 (= 10080 minutes = 7 days)
How sampling works within the training region:
Full training region (effective_train_length steps):
|<==================== effective_train_length ====================>|
bout_length_window = effective_train_length - bout_offset
Sampled windows (all the same length, shifted start positions):
|[============= bout_length_window ==============].................|
|.....[============= bout_length_window ==============]............|
|...........[============= bout_length_window ==============]......|
|.................[============= bout_length_window ==============]|
|<- bout_offset ->|
start positions
sampled here
When val_fraction > 0, the effective training length is reduced first:
Full data (bout_length):
|<--- effective_train_length --->|<--- val_length --->|
| | |
Sampling and windows operate Held out for
within this region only validation
Constraint: (1 - val_fraction) * bout_length > bout_offset — the
effective training region must be longer than bout_offset to leave room for
a meaningful forward pass.
Not burn-in: bout_offset does not control estimator warm-up.
EWMA burn-in is handled separately by max_memory_days: data is loaded
starting max_memory_days before the nominal start date, and the pool’s
warm-up fori_loop runs over all pre-start data before computing the
objective. bout_offset only controls the length and variety of sampled
windows within the training region itself.
Choosing a value: Larger values give more temporal diversity but shorter forward passes (less data per gradient step). The default 7 days provides moderate diversity for a typical multi-month training window. Very small values mean all batches evaluate nearly the same window, reducing stochasticity.
# 7-day offset (default) — windows are 7 days shorter than the full training region
run_fingerprint["bout_offset"] = 24 * 60 * 7
# 14-day offset — more start position variety, shorter windows
run_fingerprint["bout_offset"] = 24 * 60 * 14
Weight Interpolation Method
run_fingerprint.update({
"weight_interpolation_method": "linear", # "linear" or "optimal"
})
linear- Linear interpolation between weight updatesoptimal- Uses optimal interpolation that minimizes tracking error
Trade Simulation
run_fingerprint.update({
"do_trades": False, # Enable explicit trade simulation
})
When True, allows simulating specific trade sequences through the trade_array
input to calculate_reserves_with_dynamic_inputs.
Robustness and Regularisation
Early Stopping
run_fingerprint["optimisation_settings"].update({
"early_stopping": True, # Enable early stopping
"early_stopping_patience": 100, # Epochs without improvement
"early_stopping_metric": "daily_log_sharpe", # Metric to monitor
"val_fraction": 0.2, # Fraction of data for validation
})
Stochastic Weight Averaging (SWA)
run_fingerprint["optimisation_settings"].update({
"use_swa": True, # Enable SWA
"swa_start_frac": 0.75, # Start averaging at 75% of training
"swa_freq": 5, # Average every 5 epochs
})
Price Noise Augmentation
run_fingerprint.update({
"price_noise_sigma": 0.001, # Log-normal noise scale
})
Adds multiplicative log-normal noise to training prices to reduce overfitting to specific price paths. See Robustness Features for the mathematical details.
Turnover Penalty
run_fingerprint.update({
"turnover_penalty": 0.0, # Penalty weight (0 = disabled)
})
Penalises excessive weight changes during training to encourage smoother strategies.
Data Augmentation
run_fingerprint.update({
"include_flipped_training_data": False, # Flip price series
})
When True, augments the training set with time-reversed price series to
reduce directional bias.
Ensemble Training
run_fingerprint["optimisation_settings"].update({
"n_ensemble_members": 4, # Number of ensemble members
"ensemble_init_method": "lhs", # "lhs", "sobol", "grid", "gaussian"
"ensemble_init_scale": 1.0, # Perturbation scale
"ensemble_init_seed": 42, # Reproducibility seed
})
Trains multiple parameter sets simultaneously and averages their weight outputs.
Requires the ensemble hook (e.g. "ensemble__momentum").
Weight Decay
run_fingerprint["optimisation_settings"].update({
"weight_decay": 0.0, # L2 regularisation strength
})
Checkpoints
run_fingerprint["optimisation_settings"].update({
"track_checkpoints": True, # Save parameter checkpoints
"checkpoint_interval": 50, # Epochs between checkpoints
})
Checkpoint data is used for Rademacher complexity estimation during walk-forward evaluation.
Other Settings
freqData frequency string. Default
"minute". Controls how price data is loaded and interpreted.initial_memory_length_deltaOffset added to memory length for the alternative lambda (
logit_delta_lamb) parameterisation. Default0.0. Whenuse_alt_lambis True, the second EWMA has effective memoryinitial_memory_length + initial_memory_length_delta.initial_raw_widthInitial channel width parameter (log2 space) for power channel and mean reversion channel pools. Default
0.0(i.e., effective width = 1.0).initial_raw_exponentsInitial exponent parameter (squareplus space) for power channel pools. Default
0.0(i.e., effective exponent = 1.0, linear).initial_pre_exp_scalingPre-exponent scaling factor (logistic space) for the
use_pre_exp_scalingparameterisation. Default0.5.subsidary_poolsList of subsidiary pool configurations for composite (multi-rule) pools. Each entry is a dict with at minimum
tokens,rule,initial_memory_length, andinitial_k_per_day. Default[].
Implementation Notes
All settings have defaults in
quantammsim/runners/default_run_fingerprint.pySettings are validated before use
Some combinations may be invalid for certain strategies
For how the run_fingerprint is used in simulations, see train_on_historic_data() and do_run_on_historic_data().