Открыть на GitHub

Стратегия "Подброс монеты"

Обзор

Стратегия "Подброс монеты" при отсутствии позиции на каждой новой свече случайным образом выбирает открывать длинную или короткую позицию. После закрытия позиции, если сделка завершилась убытком, объем следующей сделки увеличивается по принципу мартингейла. Закрытие позиции происходит по фиксированным уровням тейк‑профита и стоп‑лосса, заданным в шагах цены, а при необходимости может использоваться трейлинг‑стоп.

Параметры

  • Volume – базовый объем первой сделки.
  • Martingale – коэффициент увеличения объема после убыточной сделки.
  • MaxVolume – максимальный допустимый объем после увеличения.
  • TakeProfit – размер тейк‑профита в шагах цены.
  • StopLoss – размер стоп‑лосса в шагах цены.
  • TrailingStart – дистанция в шагах цены для включения трейлинг‑стопа.
  • TrailingStop – расстояние трейлинг‑стопа в шагах цены.
  • CandleType – таймфрейм используемых свечей.

Логика работы

  1. На каждой закрывшейся свече стратегия проверяет наличие позиции.
  2. Если позиция открыта, рассчитывается текущая прибыль или убыток; при достижении тейк‑профита, стоп‑лосса или уровня трейлинг‑стопа позиция закрывается.
  3. При отсутствии позиции подбрасывается "монета":
    • Орёл – открытие длинной позиции.
    • Решка – открытие короткой позиции.
  4. Если предыдущая сделка была убыточной, объем следующей сделки умножается на Martingale, но не превышает MaxVolume.
  5. Трейлинг‑стоп активируется после прохождения ценой расстояния TrailingStart в прибыльную сторону.

Примечание

Пример предназначен для учебных целей и демонстрирует работу со случайными сигналами и управлением позиций средствами высокого уровня API StockSharp.

using System;
using System.Linq;
using System.Collections.Generic;

using Ecng.Common;
using Ecng.Collections;
using Ecng.Serialization;

using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;

using StockSharp.Algo;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Strategy that randomly opens long or short positions.
/// Uses a martingale multiplier after losses and manages
/// take profit, stop loss and optional trailing stop in price steps.
/// </summary>
public class CoinFlipStrategy : Strategy
{
	private readonly StrategyParam<decimal> _martingale;
	private readonly StrategyParam<decimal> _maxVolume;
	private readonly StrategyParam<int> _takeProfit;
	private readonly StrategyParam<int> _stopLoss;
	private readonly StrategyParam<int> _trailingStart;
	private readonly StrategyParam<int> _trailingStop;
	private readonly StrategyParam<DataType> _candleType;
	
	private static readonly Random _random = new();
	
	private decimal _entryPrice;
	private decimal _currentVolume;
	private decimal _trailingLevel;
	private bool _isLong;
	private bool _lastTradeLoss;
	
	
	/// <summary>
	/// Multiplier applied to volume after a losing trade.
	/// </summary>
	public decimal Martingale
	{
		get => _martingale.Value;
		set => _martingale.Value = value;
	}
	
	/// <summary>
	/// Maximum allowed volume after applying martingale.
	/// </summary>
	public decimal MaxVolume
	{
		get => _maxVolume.Value;
		set => _maxVolume.Value = value;
	}
	
	/// <summary>
	/// Take profit distance in price steps.
	/// </summary>
	public int TakeProfit
	{
		get => _takeProfit.Value;
		set => _takeProfit.Value = value;
	}
	
	/// <summary>
	/// Stop loss distance in price steps.
	/// </summary>
	public int StopLoss
	{
		get => _stopLoss.Value;
		set => _stopLoss.Value = value;
	}
	
	/// <summary>
	/// Distance in price steps to activate trailing stop.
	/// </summary>
	public int TrailingStart
	{
		get => _trailingStart.Value;
		set => _trailingStart.Value = value;
	}
	
	/// <summary>
	/// Trailing stop distance in price steps.
	/// </summary>
	public int TrailingStop
	{
		get => _trailingStop.Value;
		set => _trailingStop.Value = value;
	}
	
	/// <summary>
	/// Candle type for processing.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}
	
	/// <summary>
	/// Constructor.
	/// </summary>
	public CoinFlipStrategy()
	{
		
		_martingale = Param(nameof(Martingale), 1.8m)
		.SetGreaterThanZero()
		.SetDisplay("Martingale", "Volume multiplier after loss", "General");
		
		_maxVolume = Param(nameof(MaxVolume), 1m)
		.SetGreaterThanZero()
		.SetDisplay("Max Volume", "Upper limit for volume", "General");
		
		_takeProfit = Param(nameof(TakeProfit), 50)
		.SetGreaterThanZero()
		.SetDisplay("Take Profit", "Profit target in steps", "Risk");
		
		_stopLoss = Param(nameof(StopLoss), 25)
		.SetGreaterThanZero()
		.SetDisplay("Stop Loss", "Loss limit in steps", "Risk");
		
		_trailingStart = Param(nameof(TrailingStart), 14)
		.SetNotNegative()
		.SetDisplay("Trailing Start", "Steps to activate trailing", "Risk");
		
		_trailingStop = Param(nameof(TrailingStop), 3)
		.SetNotNegative()
		.SetDisplay("Trailing Stop", "Trailing distance in steps", "Risk");
		
		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
		.SetDisplay("Candle Type", "Time frame for analysis", "General");
	}
	
	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		return [(Security, CandleType)];
	}
	
	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();
		_entryPrice = 0m;
		_currentVolume = 0;
		_trailingLevel = 0m;
		_isLong = false;
		_lastTradeLoss = false;
	}
	
	/// <inheritdoc />
	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);
		
		_currentVolume = Volume;
		
		var subscription = SubscribeCandles(CandleType);
		subscription.Bind(ProcessCandle).Start();
		
		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawOwnTrades(area);
		}
	}
	
	private void ProcessCandle(ICandleMessage candle)
	{
		if (candle.State != CandleStates.Finished)
			return;

		if (!IsFormedAndOnlineAndAllowTrading())
			return;

		if (Position != 0)
		{
			if (_entryPrice == 0)
				return;

			// Manage open position using percent of entry
			var profitPct = _isLong
				? (candle.ClosePrice - _entryPrice) / _entryPrice * 100m
				: (_entryPrice - candle.ClosePrice) / _entryPrice * 100m;

			// Update trailing stop if in profit
			if (TrailingStart > 0 && profitPct >= TrailingStart * 0.1m)
			{
				var newLevel = _isLong
					? candle.ClosePrice * (1m - TrailingStop * 0.1m / 100m)
					: candle.ClosePrice * (1m + TrailingStop * 0.1m / 100m);

				if (_trailingLevel == 0m)
					_trailingLevel = newLevel;
				else if (_isLong && newLevel > _trailingLevel)
					_trailingLevel = newLevel;
				else if (!_isLong && newLevel < _trailingLevel)
					_trailingLevel = newLevel;
			}

			// Check exits - TP/SL as percentage (TakeProfit/10 %)
			if (profitPct >= TakeProfit * 0.1m)
			{
				ExitPosition(candle.ClosePrice, false);
				return;
			}

			if (profitPct <= -StopLoss * 0.1m)
			{
				ExitPosition(candle.ClosePrice, true);
				return;
			}

			if (_trailingLevel != 0m)
			{
				if ((_isLong && candle.ClosePrice <= _trailingLevel) ||
				(!_isLong && candle.ClosePrice >= _trailingLevel))
				{
					ExitPosition(candle.ClosePrice, candle.ClosePrice < _entryPrice);
				}
			}

			return;
		}
		
		// No open position -> decide direction
		var flip = _random.Next(0, 2);
		_isLong = flip == 0;
		
		_currentVolume = _lastTradeLoss
		? Math.Min(_currentVolume * Martingale, MaxVolume)
		: Volume;
		
		_entryPrice = candle.ClosePrice;
		_trailingLevel = 0m;
		
		if (_isLong)
		BuyMarket();
		else
		SellMarket();
	}
	
	private void ExitPosition(decimal closePrice, bool isLoss)
	{
		if (_isLong)
		{
			SellMarket();
		}
		else
		{
			BuyMarket();
		}
		
		_lastTradeLoss = isLoss;
		_entryPrice = 0m;
		_trailingLevel = 0m;
	}
}