Оптимизация стратегий
Обзор
StockSharp предоставляет встроенный механизм оптимизации параметров стратегий на исторических данных. Оптимизация позволяет автоматически перебирать различные комбинации параметров стратегии (длины индикаторов, таймфреймы, пороговые значения и т.д.) и находить наилучшие с точки зрения выбранного критерия (прибыль, просадка, количество сделок и т.д.).
Доступны два режима оптимизации:
- Полный перебор (Brute Force) -- класс
BruteForceOptimizer. Перебирает все возможные комбинации параметров или случайную выборку из них. - Генетический алгоритм -- класс
GeneticOptimizer. Использует эволюционный подход для поиска оптимальных параметров, что значительно эффективнее при большом пространстве параметров.
Оба оптимизатора наследуют от BaseOptimizer и работают асинхронно, возвращая IAsyncEnumerable с результатами по мере завершения каждой итерации.
Подготовка стратегии
Определение параметров с диапазонами оптимизации
Для оптимизации параметры стратегии должны быть объявлены через StrategyParam<T> с указанием диапазона значений. Метод SetOptimize(from, to, step) задает диапазон перебора, а SetCanOptimize(true) разрешает оптимизацию параметра:
class SmaStrategy : Strategy
{
private bool? _isShortLessThenLong;
public SmaStrategy()
{
_longSma = Param(nameof(LongSma), 80)
.SetCanOptimize(true)
.SetOptimize(50, 100, 5); // от 50 до 100 с шагом 5
_shortSma = Param(nameof(ShortSma), 30)
.SetCanOptimize(true)
.SetOptimize(20, 40, 1); // от 20 до 40 с шагом 1
_candleTimeFrame = Param<TimeSpan?>(nameof(CandleTimeFrame))
.SetCanOptimize(true)
.SetOptimize(
TimeSpan.FromMinutes(5), // от 5 минут
TimeSpan.FromMinutes(15), // до 15 минут
TimeSpan.FromMinutes(5)); // с шагом 5 минут
_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;
}
}
Поддерживаемые типы параметров
Оптимизация поддерживает следующие типы параметров:
| Тип | Пример SetOptimize |
|---|---|
int, long и другие целочисленные |
.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) |
Для параметров с дискретным набором значений (например, DataType) используйте 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)
Класс BruteForceOptimizer перебирает все возможные комбинации значений параметров. Подходит для небольших пространств параметров.
Создание и настройка
// Инструмент и портфель
var security = new Security
{
Id = "SBER@TQBR",
PriceStep = 0.01m,
};
var portfolio = Portfolio.CreateSimulator();
// Хранилище исторических данных
var storageRegistry = new StorageRegistry
{
DefaultDrive = new LocalMarketDataDrive(folder)
};
// Создание оптимизатора
var optimizer = new BruteForceOptimizer(
new CollectionSecurityProvider(new[] { security }),
new CollectionPortfolioProvider(new[] { portfolio }),
storageRegistry);
// Настройка параметров эмуляции
var settings = optimizer.EmulationSettings;
settings.MaxIterations = 100; // макс. кол-во итераций (0 = без ограничения)
settings.CommissionRules = new[] // комиссия
{
new CommissionTradeRule { Value = 0.01m },
};
// settings.BatchSize = 8; // кол-во параллельных потоков
// по умолчанию = CPU * 2
// Кэширование рыночных данных между итерациями (ускоряет работу)
optimizer.AdapterCache = new();
Запуск полного перебора
// Базовая стратегия с диапазонами оптимизации
var strategy = new SmaStrategy
{
Volume = 1,
Security = security,
Portfolio = portfolio,
};
// Выбрать параметры для оптимизации
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 };
// Сгенерировать все комбинации параметров
var strategies = strategy.ToBruteForce(optimizeParams, out _, out var totalCount);
// Запустить оптимизацию
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 -- стратегия с результатами после бэктеста
Console.WriteLine($"PnL={s.PnL}, LongSma={s.Parameters["LongSma"].Value}, " +
$"ShortSma={s.Parameters["ShortSma"].Value}");
}
Случайная выборка
Если полный перебор всех комбинаций занимает слишком много времени, можно использовать случайную выборку. Метод ToBruteForceRandom генерирует заданное количество случайных комбинаций:
var randomCount = 50; // количество случайных комбинаций
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}");
}
Генетическая оптимизация
Класс GeneticOptimizer реализует генетический алгоритм, который значительно эффективнее полного перебора при большом количестве параметров. Алгоритм автоматически сходится к оптимальным значениям за меньшее число итераций.
Создание и настройка
var optimizer = new GeneticOptimizer(
new CollectionSecurityProvider(new[] { security }),
new CollectionPortfolioProvider(new[] { portfolio }),
storageRegistry,
Paths.FileSystem); // файловая система для формулы фитнеса
optimizer.AdapterCache = new();
// Настройка генетического алгоритма
optimizer.Settings.Population = 8; // размер популяции
optimizer.Settings.PopulationMax = 16; // максимальный размер популяции
optimizer.Settings.GenerationsMax = 20; // макс. кол-во поколений
optimizer.Settings.GenerationsStagnation = 5; // остановка при стагнации (кол-во поколений без улучшения)
optimizer.Settings.MutationProbability = 0.1m;
optimizer.Settings.CrossoverProbability = 0.75m;
optimizer.Settings.Fitness = "PnL"; // формула фитнеса (по умолчанию PnL)
optimizer.EmulationSettings.MaxIterations = 100;
Параметры генетического алгоритма
| Свойство | Описание | По умолчанию |
|---|---|---|
Population |
Начальный размер популяции | 8 |
PopulationMax |
Максимальный размер популяции | 16 |
GenerationsMax |
Максимальное кол-во поколений (0 = без ограничения) | 20 |
GenerationsStagnation |
Остановка при стагнации N поколений (0 = отключено) | 5 |
MutationProbability |
Вероятность мутации (0..1) | 0.1 |
CrossoverProbability |
Вероятность скрещивания (0..1) | 0.75 |
Fitness |
Формула фитнес-функции | "PnL" |
Selection |
Оператор отбора | TournamentSelection |
Crossover |
Оператор скрещивания | OnePointCrossover |
Mutation |
Оператор мутации | UniformMutation |
Reinsertion |
Стратегия замены поколений | ElitistReinsertion |
Формула фитнес-функции
Свойство Fitness задает формулу для оценки стратегии. Поддерживаются статистические параметры стратегии в виде сокращений:
| Сокращение | Статистический параметр |
|---|---|
PnL |
Чистая прибыль |
MaxDD |
Максимальная просадка |
MaxRelDD |
Максимальная относительная просадка |
WinTrades |
Выигрышные сделки |
LosTrades |
Убыточные сделки |
Recovery |
Коэффициент восстановления |
Ret |
Доходность |
TCount |
Количество сделок |
AvgTPnL |
Средняя прибыль на сделку |
Можно комбинировать формулы, например: "PnL - MaxDD" или "Recovery".
Запуск генетической оптимизации
var strategy = new SmaStrategy
{
Volume = 1,
Security = security,
Portfolio = portfolio,
};
// Подготовить параметры для генетического оптимизатора
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 конвертирует параметры стратегии в формат генетического оптимизатора.
// Для параметров с дискретным набором значений (например, TimeSpan?) можно передать
// явный список через кортеж (param, values):
var geneticParams = strategy.ToGeneticParameters(new (IStrategyParam, IEnumerable)[]
{
(tfParam, new[] { TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(15) }),
(longParam, null), // null = использовать диапазон из SetOptimize
(shortParam, null),
});
// Запуск
var cts = new CancellationTokenSource();
await foreach (var (s, parameters) in optimizer.RunAsync(
startTime, stopTime, strategy, geneticParams, cancellationToken: cts.Token))
{
Console.WriteLine($"PnL={s.PnL}");
}
Общие настройки оптимизатора
Класс OptimizerSettings (доступен через optimizer.EmulationSettings) содержит настройки, общие для обоих типов оптимизации:
| Свойство | Описание | По умолчанию |
|---|---|---|
BatchSize |
Количество параллельно тестируемых стратегий | CPU * 2 |
MaxIterations |
Максимальное количество итераций (0 = без ограничения) | 0 |
MaxMessageCount |
Максимальное количество обработанных сообщений (-1 = без ограничения) | -1 |
CommissionRules |
Правила расчета комиссии | null |
Кэширование данных
Свойство AdapterCache позволяет кэшировать рыночные данные между итерациями, что значительно ускоряет оптимизацию, так как данные загружаются из хранилища только один раз:
optimizer.AdapterCache = new MarketDataStorageCache();
События оптимизатора
| Событие | Описание |
|---|---|
SingleProgressChanged |
Вызывается при изменении прогресса отдельной итерации. Параметры: (Strategy, IStrategyParam[], int progress). Прогресс 100 означает завершение итерации. |
StrategyInitialized |
Вызывается после инициализации стратегии перед запуском бэктеста. |
ConnectorInitialized |
Вызывается после создания коннектора перед подключением. Позволяет настроить параметры HistoryEmulationConnector. |
optimizer.SingleProgressChanged += (strategy, parameters, progress) =>
{
if (progress == 100)
Console.WriteLine($"Итерация завершена: PnL={strategy.PnL}");
};
Пауза и остановка
Оптимизацию можно приостановить и возобновить:
// Приостановить (текущие итерации завершатся, новые не начнутся)
optimizer.Pause();
// Возобновить
optimizer.Resume();
// Проверить состояние
bool isPaused = optimizer.IsPaused;
Для полной остановки отмените CancellationToken:
cts.Cancel();
Полный пример (консольное приложение)
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;
// Настройка инструмента и портфеля
var security = new Security
{
Id = "SBER@TQBR",
PriceStep = 0.01m,
};
var portfolio = Portfolio.CreateSimulator();
// Хранилище данных
var storageRegistry = new StorageRegistry
{
DefaultDrive = new LocalMarketDataDrive(Paths.HistoryDataPath)
};
// Создание оптимизатора (полный перебор)
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();
// Настройка стратегии
var strategy = new SmaStrategy
{
Volume = 1,
Security = security,
Portfolio = portfolio,
};
// Параметры для оптимизации
var longParam = (StrategyParam<int>)strategy.Parameters[nameof(strategy.LongSma)];
var shortParam = (StrategyParam<int>)strategy.Parameters[nameof(strategy.ShortSma)];
var optimizeParams = new IStrategyParam[] { longParam, shortParam };
// Генерация комбинаций
var strategies = strategy.ToBruteForce(optimizeParams, out _, out var totalCount);
Console.WriteLine($"Всего итераций: {totalCount}");
// Запуск оптимизации
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($"\nЛучший результат: PnL={bestPnL:F2}");
foreach (var p in bestStrategy.Parameters)
Console.WriteLine($" {p.Id} = {p.Value}");
}
См. также
- Тестирование на исторических данных
- Пример:
Samples/07_Testing/02_Optimization