Strategieoptimierung

Überblick

StockSharp stellt einen integrierten Mechanismus zur Optimierung von Strategieparametern auf historischen Daten bereit. Die Optimierung durchläuft automatisch verschiedene Kombinationen von Strategieparametern, etwa Indikatorlängen, Zeitrahmen, Schwellenwerte und andere Einstellungen, und hilft, das beste Ergebnis nach dem gewählten Kriterium zu finden, beispielsweise Gewinn, Drawdown, Anzahl der Trades usw.

Es stehen zwei Optimierungsmodi zur Verfügung:

  • Brute Force -- die Klasse BruteForceOptimizer. Durchläuft alle möglichen Parameterkombinationen oder eine zufällige Teilmenge davon.
  • Genetischer Algorithmus -- die Klasse GeneticOptimizer. Verwendet einen evolutionären Ansatz zum Finden optimaler Parameter, der bei großen Parameterräumen deutlich effizienter ist.

Beide Optimierer erben von BaseOptimizer und arbeiten asynchron. Sie geben ein IAsyncEnumerable mit Ergebnissen zurück, sobald die jeweilige Iteration abgeschlossen ist.

Strategie vorbereiten

Parameter mit Optimierungsbereichen definieren

Um eine Strategie zu optimieren, definieren Sie ihre Parameter über StrategyParam<T> und geben Wertebereiche an. Die Methode SetOptimize(from, to, step) legt den zu durchlaufenden Bereich fest, und SetCanOptimize(true) aktiviert die Optimierung für den Parameter:

class SmaStrategy : Strategy
{
    private bool? _isShortLessThenLong;

    public SmaStrategy()
    {
        _longSma = Param(nameof(LongSma), 80)
            .SetCanOptimize(true)
            .SetOptimize(50, 100, 5);      // von 50 bis 100 mit Schrittweite 5

        _shortSma = Param(nameof(ShortSma), 30)
            .SetCanOptimize(true)
            .SetOptimize(20, 40, 1);        // von 20 bis 40 mit Schrittweite 1

        _candleTimeFrame = Param<TimeSpan?>(nameof(CandleTimeFrame))
            .SetCanOptimize(true)
            .SetOptimize(
                TimeSpan.FromMinutes(5),    // ab 5 Minuten
                TimeSpan.FromMinutes(15),   // bis 15 Minuten
                TimeSpan.FromMinutes(5));   // mit Schrittweite 5 Minuten

        _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;
    }
}

Unterstützte Parametertypen

Die Optimierung unterstützt die folgenden Parametertypen:

Typ Beispiel für SetOptimize
int, long und andere ganzzahlige Typen .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)

Für Parameter mit einer diskreten Wertemenge, etwa DataType, verwenden Sie 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(),
    });

Brute-Force-Optimierung

Die Klasse BruteForceOptimizer durchläuft alle möglichen Kombinationen von Parameterwerten. Dieser Modus eignet sich für kleine Parameterräume.

Optimierer erstellen und konfigurieren

// Instrument und Portfolio.
var security = new Security
{
    Id = "AAPL@NASDAQ",
    PriceStep = 0.01m,
};

var portfolio = Portfolio.CreateSimulator();

// Speicher für historische Daten.
var storageRegistry = new StorageRegistry
{
    DefaultDrive = new LocalMarketDataDrive(folder)
};

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

// Emulationsparameter konfigurieren.
var settings = optimizer.EmulationSettings;
settings.MaxIterations = 100;                          // maximale Iterationen (0 = unbegrenzt)
settings.CommissionRules = new[]                       // Kommission
{
    new CommissionTradeRule { Value = 0.01m },
};
// settings.BatchSize = 8;                             // Anzahl paralleler Threads
                                                       // Standard = CPU * 2

// Marktdaten zwischen Iterationen zwischenspeichern, um die Optimierung zu beschleunigen.
optimizer.AdapterCache = new();

Brute-Force-Optimierung ausführen

// Basisstrategie mit Optimierungsbereichen.
var strategy = new SmaStrategy
{
    Volume = 1,
    Security = security,
    Portfolio = portfolio,
};

// Zu optimierende Parameter auswählen.
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 };

// Alle Parameterkombinationen erzeugen.
var strategies = strategy.ToBruteForce(optimizeParams, out _, out var totalCount);

// Optimierung ausführen.
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 ist die Strategie mit Ergebnissen nach dem Backtest.
    Console.WriteLine($"PnL={s.PnL}, LongSma={s.Parameters["LongSma"].Value}, " +
                      $"ShortSma={s.Parameters["ShortSma"].Value}");
}

Zufallsstichprobe

Wenn das Durchlaufen aller Kombinationen zu viel Zeit benötigt, verwenden Sie eine Zufallsstichprobe. Die Methode ToBruteForceRandom erzeugt die angegebene Anzahl zufälliger Kombinationen:

var randomCount = 50; // Anzahl zufälliger Kombinationen

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}");
}

Genetische Optimierung

Die Klasse GeneticOptimizer implementiert einen genetischen Algorithmus, der bei einer großen Anzahl von Parametern deutlich effizienter ist als Brute Force. Der Algorithmus konvergiert automatisch in weniger Iterationen zu optimalen Werten.

Optimierer erstellen und konfigurieren

var optimizer = new GeneticOptimizer(
    new CollectionSecurityProvider(new[] { security }),
    new CollectionPortfolioProvider(new[] { portfolio }),
    storageRegistry,
    Paths.FileSystem);    // Dateisystem für die Fitness-Formel

optimizer.AdapterCache = new();

// Genetischen Algorithmus konfigurieren.
optimizer.Settings.Population = 8;            // Populationsgröße
optimizer.Settings.PopulationMax = 16;        // maximale Populationsgröße
optimizer.Settings.GenerationsMax = 20;       // maximale Anzahl Generationen
optimizer.Settings.GenerationsStagnation = 5; // nach N Generationen ohne Verbesserung stoppen
optimizer.Settings.MutationProbability = 0.1m;
optimizer.Settings.CrossoverProbability = 0.75m;
optimizer.Settings.Fitness = "PnL";           // Fitness-Formel (standardmäßig PnL)

optimizer.EmulationSettings.MaxIterations = 100;

Parameter des genetischen Algorithmus

Eigenschaft Beschreibung Standard
Population Größe der Anfangspopulation 8
PopulationMax Maximale Populationsgröße 16
GenerationsMax Maximale Anzahl von Generationen (0 = unbegrenzt) 20
GenerationsStagnation Nach N stagnierenden Generationen stoppen (0 = deaktiviert) 5
MutationProbability Mutationswahrscheinlichkeit (0..1) 0.1
CrossoverProbability Crossover-Wahrscheinlichkeit (0..1) 0.75
Fitness Formel der Fitness-Funktion "PnL"
Selection Selektionsoperator TournamentSelection
Crossover Crossover-Operator OnePointCrossover
Mutation Mutationsoperator UniformMutation
Reinsertion Strategie zum Ersetzen von Generationen ElitistReinsertion

Fitness-Formel

Die Eigenschaft Fitness definiert die Formel, mit der eine Strategie bewertet wird. Strategiestatistiken sind über Abkürzungen verfügbar:

Abkürzung Strategiestatistik
PnL Nettogewinn
MaxDD Maximaler Drawdown
MaxRelDD Maximaler relativer Drawdown
WinTrades Gewinntrades
LosTrades Verlusttrades
Recovery Recovery-Faktor
Ret Rendite
TCount Anzahl der Trades
AvgTPnL Durchschnittlicher Gewinn pro Trade

Formeln können kombiniert werden, zum Beispiel: "PnL - MaxDD" oder "Recovery".

Genetische Optimierung ausführen

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

// Parameter für den genetischen Optimierer vorbereiten.
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 konvertiert Strategieparameter in das Format des genetischen Optimierers.
// Für Parameter mit diskreter Wertemenge, etwa TimeSpan?, übergeben Sie eine explizite
// Liste über ein Tupel (param, values):
var geneticParams = strategy.ToGeneticParameters(new (IStrategyParam, IEnumerable)[]
{
    (tfParam, new[] { TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(15) }),
    (longParam, null),   // null = den Bereich aus SetOptimize verwenden
    (shortParam, null),
});

// Optimierung ausführen.
var cts = new CancellationTokenSource();

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

Gemeinsame Optimierereinstellungen

Die Klasse OptimizerSettings, verfügbar über optimizer.EmulationSettings, enthält Einstellungen, die für beide Optimierungsarten gelten:

Eigenschaft Beschreibung Standard
BatchSize Anzahl parallel getesteter Strategien CPU * 2
MaxIterations Maximale Anzahl von Iterationen (0 = unbegrenzt) 0
MaxMessageCount Maximale Anzahl verarbeiteter Nachrichten (-1 = unbegrenzt) -1
CommissionRules Regeln zur Kommissionsberechnung null

Daten-Caching

Die Eigenschaft AdapterCache speichert Marktdaten zwischen Iterationen zwischen. Das beschleunigt die Optimierung erheblich, da Daten nur einmal aus dem Speicher geladen werden:

optimizer.AdapterCache = new MarketDataStorageCache();

Optimiererereignisse

Ereignis Beschreibung
SingleProgressChanged Wird aufgerufen, wenn sich der Fortschritt einer einzelnen Iteration ändert. Parameter: (Strategy, IStrategyParam[], int progress). Fortschritt 100 bedeutet, dass die Iteration abgeschlossen ist.
StrategyInitialized Wird aufgerufen, nachdem die Strategie initialisiert wurde und bevor der Backtest startet.
ConnectorInitialized Wird aufgerufen, nachdem der Connector erstellt wurde und bevor er eine Verbindung herstellt. Ermöglicht die Konfiguration von Parametern des HistoryEmulationConnector.
optimizer.SingleProgressChanged += (strategy, parameters, progress) =>
{
    if (progress == 100)
        Console.WriteLine($"Iteration abgeschlossen: PnL={strategy.PnL}");
};

Pausieren und stoppen

Die Optimierung kann pausiert und fortgesetzt werden:

// Pausieren. Laufende Iterationen werden abgeschlossen, neue werden nicht gestartet.
optimizer.Pause();

// Fortsetzen.
optimizer.Resume();

// Zustand prüfen.
bool isPaused = optimizer.IsPaused;

Um die Optimierung vollständig zu stoppen, brechen Sie das CancellationToken ab:

cts.Cancel();

Vollständiges Beispiel (Konsolenanwendung)

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;

// Instrument und Portfolio konfigurieren.
var security = new Security
{
    Id = "AAPL@NASDAQ",
    PriceStep = 0.01m,
};

var portfolio = Portfolio.CreateSimulator();

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

// Optimierer erstellen (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();

// Strategie konfigurieren.
var strategy = new SmaStrategy
{
    Volume = 1,
    Security = security,
    Portfolio = portfolio,
};

// Zu optimierende Parameter.
var longParam = (StrategyParam<int>)strategy.Parameters[nameof(strategy.LongSma)];
var shortParam = (StrategyParam<int>)strategy.Parameters[nameof(strategy.ShortSma)];
var optimizeParams = new IStrategyParam[] { longParam, shortParam };

// Kombinationen erzeugen.
var strategies = strategy.ToBruteForce(optimizeParams, out _, out var totalCount);

Console.WriteLine($"Iterationen insgesamt: {totalCount}");

// Optimierung ausführen.
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($"\nBestes Ergebnis: PnL={bestPnL:F2}");
    foreach (var p in bestStrategy.Parameters)
        Console.WriteLine($"  {p.Id} = {p.Value}");
}

Siehe auch