Otimização de estratégias

Visão geral

O StockSharp fornece um mecanismo incorporado para otimizar parâmetros de estratégias em dados históricos. A otimização percorre automaticamente diferentes combinações de parâmetros da estratégia, como comprimentos de indicadores, períodos, valores de limiar e outras definições, e ajuda a encontrar o melhor resultado para o critério selecionado, como lucro, drawdown, número de transações, e assim por diante.

Estão disponíveis dois modos de otimização:

  • Brute force -- a classe BruteForceOptimizer. Percorre todas as combinações possíveis de parâmetros, ou um subconjunto aleatório delas.
  • Genetic algorithm -- a classe GeneticOptimizer. Usa uma abordagem evolutiva para encontrar parâmetros ótimos, o que é muito mais eficiente para espaços de parâmetros grandes.

Ambos os otimizadores herdam de BaseOptimizer e trabalham de forma assíncrona, devolvendo um IAsyncEnumerable com resultados à medida que cada iteração termina.

Preparar uma estratégia

Definir parâmetros com intervalos de otimização

Para otimizar uma estratégia, defina os seus parâmetros através de StrategyParam<T> e especifique intervalos de valores. O método SetOptimize(from, to, step) define o intervalo a percorrer, e SetCanOptimize(true) ativa a otimização para o parâmetro:

class SmaStrategy : Strategy
{
    private bool? _isShortLessThenLong;

    public SmaStrategy()
    {
        _longSma = Param(nameof(LongSma), 80)
            .SetCanOptimize(true)
            .SetOptimize(50, 100, 5);      // de 50 a 100 com passo de 5

        _shortSma = Param(nameof(ShortSma), 30)
            .SetCanOptimize(true)
            .SetOptimize(20, 40, 1);        // de 20 a 40 com passo de 1

        _candleTimeFrame = Param<TimeSpan?>(nameof(CandleTimeFrame))
            .SetCanOptimize(true)
            .SetOptimize(
                TimeSpan.FromMinutes(5),    // a partir de 5 minutos
                TimeSpan.FromMinutes(15),   // até 15 minutos
                TimeSpan.FromMinutes(5));   // com passo de 5 minutos

        _candleType = Param(nameof(CandleType),
            TimeSpan.FromMinutes(1).TimeFrame()).SetRequired();
    }

    private readonly StrategyParam<int> _longSma;
    public int LongSma
    {
        get => _longSma.Value;
        set => _longSma.Value = value;
    }

    private readonly StrategyParam<int> _shortSma;
    public int ShortSma
    {
        get => _shortSma.Value;
        set => _shortSma.Value = value;
    }

    private readonly StrategyParam<TimeSpan?> _candleTimeFrame;
    public TimeSpan? CandleTimeFrame
    {
        get => _candleTimeFrame.Value;
        set => _candleTimeFrame.Value = value;
    }

    private readonly StrategyParam<DataType> _candleType;
    public DataType CandleType
    {
        get => _candleType.Value;
        set => _candleType.Value = value;
    }

    protected override void OnStarted2(DateTime time)
    {
        base.OnStarted2(time);

        var dt = CandleTimeFrame is null
            ? CandleType
            : DataType.Create(CandleType.MessageType, CandleTimeFrame);

        var subscription = new Subscription(dt, Security)
        {
            MarketData =
            {
                IsFinishedOnly = true,
            }
        };

        var longSma = new SMA { Length = LongSma };
        var shortSma = new SMA { Length = ShortSma };

        SubscribeCandles(subscription)
            .Bind(longSma, shortSma, OnProcess)
            .Start();
    }

    private void OnProcess(ICandleMessage candle, decimal longValue, decimal shortValue)
    {
        if (candle.State != CandleStates.Finished)
            return;

        var isShortLessThenLong = shortValue < longValue;

        if (_isShortLessThenLong == null)
        {
            _isShortLessThenLong = isShortLessThenLong;
        }
        else if (_isShortLessThenLong != isShortLessThenLong)
        {
            var direction = isShortLessThenLong ? Sides.Sell : Sides.Buy;
            var volume = Position == 0 ? Volume : Position.Abs().Min(Volume) * 2;
            var price = candle.ClosePrice;

            if (direction == Sides.Buy)
                BuyLimit(price, volume);
            else
                SellLimit(price, volume);

            _isShortLessThenLong = isShortLessThenLong;
        }
    }

    protected override void OnReseted()
    {
        base.OnReseted();
        _isShortLessThenLong = null;
    }
}

Tipos de parâmetros suportados

A otimização suporta os seguintes tipos de parâmetros:

Tipo Exemplo de SetOptimize
int, long e outros tipos inteiros .SetOptimize(10, 100, 5)
decimal, double, float .SetOptimize(0.01m, 0.10m, 0.01m)
TimeSpan .SetOptimize(TimeSpan.FromMinutes(1), TimeSpan.FromMinutes(30), TimeSpan.FromMinutes(1))
Unit .SetOptimize(new Unit(1, UnitTypes.Percent), new Unit(5, UnitTypes.Percent), new Unit(0.5m, UnitTypes.Percent))
bool .SetOptimize(false, true)

Para parâmetros com um conjunto discreto de valores, como DataType, use SetOptimizeValues:

_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
    .SetCanOptimize(true)
    .SetOptimizeValues(new[]
    {
        TimeSpan.FromMinutes(5).TimeFrame(),
        TimeSpan.FromMinutes(15).TimeFrame(),
        TimeSpan.FromMinutes(30).TimeFrame(),
    });

Otimização brute force

A classe BruteForceOptimizer percorre todas as combinações possíveis de valores dos parâmetros. Este modo é adequado para espaços de parâmetros pequenos.

Criar e configurar o otimizador

// Instrumento e carteira.
var security = new Security
{
    Id = "AAPL@NASDAQ",
    PriceStep = 0.01m,
};

var portfolio = Portfolio.CreateSimulator();

// Armazenamento de dados históricos.
var storageRegistry = new StorageRegistry
{
    DefaultDrive = new LocalMarketDataDrive(folder)
};

// Criar o otimizador.
var optimizer = new BruteForceOptimizer(
    new CollectionSecurityProvider(new[] { security }),
    new CollectionPortfolioProvider(new[] { portfolio }),
    storageRegistry);

// Configurar parâmetros de emulação.
var settings = optimizer.EmulationSettings;
settings.MaxIterations = 100;                          // máximo de iterações (0 = ilimitado)
settings.CommissionRules = new[]                       // comissão
{
    new CommissionTradeRule { Value = 0.01m },
};
// settings.BatchSize = 8;                             // número de threads paralelas
                                                       // predefinição = CPU * 2

// Colocar dados de mercado em cache entre iterações para acelerar a otimização.
optimizer.AdapterCache = new();

Executar otimização brute force

// Estratégia base com intervalos de otimização.
var strategy = new SmaStrategy
{
    Volume = 1,
    Security = security,
    Portfolio = portfolio,
};

// Selecionar parâmetros a otimizar.
var longParam = (StrategyParam<int>)strategy.Parameters[nameof(strategy.LongSma)];
var shortParam = (StrategyParam<int>)strategy.Parameters[nameof(strategy.ShortSma)];
var tfParam = (StrategyParam<TimeSpan?>)strategy.Parameters[nameof(strategy.CandleTimeFrame)];

var optimizeParams = new IStrategyParam[] { longParam, shortParam, tfParam };

// Gerar todas as combinações de parâmetros.
var strategies = strategy.ToBruteForce(optimizeParams, out _, out var totalCount);

// Executar otimização.
var startTime = new DateTime(2020, 1, 1);
var stopTime = new DateTime(2020, 12, 31);
var cts = new CancellationTokenSource();

await foreach (var (s, parameters) in optimizer.RunAsync(startTime, stopTime, strategies, cts.Token))
{
    // s é a estratégia com resultados após o backtesting.
    Console.WriteLine($"PnL={s.PnL}, LongSma={s.Parameters["LongSma"].Value}, " +
                      $"ShortSma={s.Parameters["ShortSma"].Value}");
}

Amostragem aleatória

Se percorrer todas as combinações demorar demasiado tempo, use amostragem aleatória. O método ToBruteForceRandom gera o número especificado de combinações aleatórias:

var randomCount = 50; // número de combinações aleatórias

var strategies = strategy.ToBruteForceRandom(
    optimizeParams,
    randomCount,
    out _,
    out var totalCount);

await foreach (var (s, parameters) in optimizer.RunAsync(startTime, stopTime, strategies, cts.Token))
{
    Console.WriteLine($"PnL={s.PnL}");
}

Otimização genética

A classe GeneticOptimizer implementa um algoritmo genético que é muito mais eficiente do que brute force quando o número de parâmetros é grande. O algoritmo converge automaticamente para valores ótimos em menos iterações.

Criar e configurar o otimizador

var optimizer = new GeneticOptimizer(
    new CollectionSecurityProvider(new[] { security }),
    new CollectionPortfolioProvider(new[] { portfolio }),
    storageRegistry,
    Paths.FileSystem);    // sistema de ficheiros para a fórmula de fitness

optimizer.AdapterCache = new();

// Configurar o algoritmo genético.
optimizer.Settings.Population = 8;            // tamanho da população
optimizer.Settings.PopulationMax = 16;        // tamanho máximo da população
optimizer.Settings.GenerationsMax = 20;       // máximo de gerações
optimizer.Settings.GenerationsStagnation = 5; // parar após N gerações sem melhoria
optimizer.Settings.MutationProbability = 0.1m;
optimizer.Settings.CrossoverProbability = 0.75m;
optimizer.Settings.Fitness = "PnL";           // fórmula de fitness (PnL por predefinição)

optimizer.EmulationSettings.MaxIterations = 100;

Parâmetros do algoritmo genético

Propriedade Descrição Predefinição
Population Tamanho inicial da população 8
PopulationMax Tamanho máximo da população 16
GenerationsMax Número máximo de gerações (0 = ilimitado) 20
GenerationsStagnation Parar após N gerações estagnadas (0 = desativado) 5
MutationProbability Probabilidade de mutação (0..1) 0.1
CrossoverProbability Probabilidade de crossover (0..1) 0.75
Fitness Fórmula da função de fitness "PnL"
Selection Operador de seleção TournamentSelection
Crossover Operador de crossover OnePointCrossover
Mutation Operador de mutação UniformMutation
Reinsertion Estratégia de substituição de geração ElitistReinsertion

Fórmula de fitness

A propriedade Fitness define a fórmula usada para avaliar uma estratégia. As estatísticas da estratégia estão disponíveis através de abreviaturas:

Abreviatura Estatística da estratégia
PnL Lucro líquido
MaxDD Drawdown máximo
MaxRelDD Drawdown relativo máximo
WinTrades Transações vencedoras
LosTrades Transações perdedoras
Recovery Fator de recuperação
Ret Retorno
TCount Número de transações
AvgTPnL Lucro médio por transação

As fórmulas podem ser combinadas, por exemplo: "PnL - MaxDD" ou "Recovery".

Executar otimização genética

var strategy = new SmaStrategy
{
    Volume = 1,
    Security = security,
    Portfolio = portfolio,
};

// Preparar parâmetros para o otimizador genético.
var longParam = (StrategyParam<int>)strategy.Parameters[nameof(strategy.LongSma)];
var shortParam = (StrategyParam<int>)strategy.Parameters[nameof(strategy.ShortSma)];
var tfParam = (StrategyParam<TimeSpan?>)strategy.Parameters[nameof(strategy.CandleTimeFrame)];

// ToGeneticParameters converte parâmetros da estratégia para o formato do otimizador genético.
// Para parâmetros com um conjunto discreto de valores, como TimeSpan?, passe uma lista
// explícita através de um tuplo (param, values):
var geneticParams = strategy.ToGeneticParameters(new (IStrategyParam, IEnumerable)[]
{
    (tfParam, new[] { TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(15) }),
    (longParam, null),   // null = usar o intervalo de SetOptimize
    (shortParam, null),
});

// Executar otimização.
var cts = new CancellationTokenSource();

await foreach (var (s, parameters) in optimizer.RunAsync(
    startTime, stopTime, strategy, geneticParams, cancellationToken: cts.Token))
{
    Console.WriteLine($"PnL={s.PnL}");
}

Definições comuns do otimizador

A classe OptimizerSettings, disponível através de optimizer.EmulationSettings, contém definições comuns aos dois tipos de otimização:

Propriedade Descrição Predefinição
BatchSize Número de estratégias testadas em paralelo CPU * 2
MaxIterations Número máximo de iterações (0 = ilimitado) 0
MaxMessageCount Número máximo de mensagens processadas (-1 = ilimitado) -1
CommissionRules Regras de cálculo de comissão null

Cache de dados

A propriedade AdapterCache coloca dados de mercado em cache entre iterações. Isto acelera significativamente a otimização, porque os dados são carregados do armazenamento apenas uma vez:

optimizer.AdapterCache = new MarketDataStorageCache();

Eventos do otimizador

Evento Descrição
SingleProgressChanged Chamado quando o progresso muda para uma única iteração. Parâmetros: (Strategy, IStrategyParam[], int progress). Progresso 100 significa que a iteração está concluída.
StrategyInitialized Chamado depois de a estratégia ser inicializada e antes de o backtest começar.
ConnectorInitialized Chamado depois de o conector ser criado e antes de se ligar. Permite configurar parâmetros de HistoryEmulationConnector.
optimizer.SingleProgressChanged += (strategy, parameters, progress) =>
{
    if (progress == 100)
        Console.WriteLine($"Iteration complete: PnL={strategy.PnL}");
};

Pausa e paragem

A otimização pode ser pausada e retomada:

// Pausar. As iterações atuais terminarão; novas iterações não serão iniciadas.
optimizer.Pause();

// Retomar.
optimizer.Resume();

// Verificar estado.
bool isPaused = optimizer.IsPaused;

Para parar completamente a otimização, cancele o CancellationToken:

cts.Cancel();

Exemplo completo (aplicação de consola)

using System;
using System.Linq;
using System.Threading;

using StockSharp.Algo;
using StockSharp.Algo.Storages;
using StockSharp.Algo.Strategies;
using StockSharp.Algo.Strategies.Optimization;
using StockSharp.Algo.Commissions;
using StockSharp.BusinessEntities;
using StockSharp.Configuration;
using StockSharp.Messages;

// Configurar o instrumento e a carteira.
var security = new Security
{
    Id = "AAPL@NASDAQ",
    PriceStep = 0.01m,
};

var portfolio = Portfolio.CreateSimulator();

// Armazenamento de dados.
var storageRegistry = new StorageRegistry
{
    DefaultDrive = new LocalMarketDataDrive(Paths.HistoryDataPath)
};

// Criar o otimizador (brute force).
var optimizer = new BruteForceOptimizer(
    new CollectionSecurityProvider(new[] { security }),
    new CollectionPortfolioProvider(new[] { portfolio }),
    storageRegistry);

optimizer.EmulationSettings.MaxIterations = 100;
optimizer.EmulationSettings.CommissionRules = new[]
{
    new CommissionTradeRule { Value = 0.01m },
};
optimizer.AdapterCache = new();

// Configurar a estratégia.
var strategy = new SmaStrategy
{
    Volume = 1,
    Security = security,
    Portfolio = portfolio,
};

// Parâmetros a otimizar.
var longParam = (StrategyParam<int>)strategy.Parameters[nameof(strategy.LongSma)];
var shortParam = (StrategyParam<int>)strategy.Parameters[nameof(strategy.ShortSma)];
var optimizeParams = new IStrategyParam[] { longParam, shortParam };

// Gerar combinações.
var strategies = strategy.ToBruteForce(optimizeParams, out _, out var totalCount);

Console.WriteLine($"Total iterations: {totalCount}");

// Executar otimização.
var startTime = Paths.HistoryBeginDate;
var stopTime = Paths.HistoryEndDate;
var cts = new CancellationTokenSource();

var bestPnL = decimal.MinValue;
Strategy bestStrategy = null;

await foreach (var (s, parameters) in optimizer.RunAsync(startTime, stopTime, strategies, cts.Token))
{
    var pnl = s.PnL;
    var paramStr = string.Join(", ", parameters.Select(p => $"{p.Id}={p.Value}"));
    Console.WriteLine($"[{paramStr}] PnL={pnl:F2}");

    if (pnl > bestPnL)
    {
        bestPnL = pnl;
        bestStrategy = s;
    }
}

if (bestStrategy != null)
{
    Console.WriteLine($"\nBest result: PnL={bestPnL:F2}");
    foreach (var p in bestStrategy.Parameters)
        Console.WriteLine($"  {p.Id} = {p.Value}");
}

Ver também