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;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Parabolic SAR first dot reversal strategy.
/// Opens a position when Parabolic SAR flips relative to the close and protects it with classic stops.
/// </summary>
public class ParabolicSarFirstDotStrategy : Strategy
{
private readonly StrategyParam<decimal> _tradeVolume;
private readonly StrategyParam<int> _stopLossPoints;
private readonly StrategyParam<int> _takeProfitPoints;
private readonly StrategyParam<bool> _useStopMultiplier;
private readonly StrategyParam<decimal> _sarAccelerationStep;
private readonly StrategyParam<decimal> _sarAccelerationMax;
private readonly StrategyParam<DataType> _candleType;
private decimal? _longStop;
private decimal? _longTake;
private decimal? _shortStop;
private decimal? _shortTake;
private bool? _prevIsSarAbovePrice;
private decimal _priceStep;
private DateTimeOffset _lastTradeTime;
/// <summary>
/// Trading volume in lots.
/// </summary>
public decimal TradeVolume
{
get => _tradeVolume.Value;
set
{
_tradeVolume.Value = value;
Volume = value;
}
}
/// <summary>
/// Stop-loss distance expressed in Parabolic SAR points.
/// </summary>
public int StopLossPoints
{
get => _stopLossPoints.Value;
set => _stopLossPoints.Value = value;
}
/// <summary>
/// Take-profit distance expressed in Parabolic SAR points.
/// </summary>
public int TakeProfitPoints
{
get => _takeProfitPoints.Value;
set => _takeProfitPoints.Value = value;
}
/// <summary>
/// Multiply stop distances by ten to mirror the MetaTrader implementation.
/// </summary>
public bool UseStopMultiplier
{
get => _useStopMultiplier.Value;
set => _useStopMultiplier.Value = value;
}
/// <summary>
/// Initial acceleration factor for Parabolic SAR.
/// </summary>
public decimal SarAccelerationStep
{
get => _sarAccelerationStep.Value;
set => _sarAccelerationStep.Value = value;
}
/// <summary>
/// Maximum acceleration factor for Parabolic SAR.
/// </summary>
public decimal SarAccelerationMax
{
get => _sarAccelerationMax.Value;
set => _sarAccelerationMax.Value = value;
}
/// <summary>
/// Candle type used for indicator calculations.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initializes a new instance of the <see cref="ParabolicSarFirstDotStrategy"/>.
/// </summary>
public ParabolicSarFirstDotStrategy()
{
_tradeVolume = Param(nameof(TradeVolume), 0.1m)
.SetDisplay("Volume", "Order volume in lots", "General")
.SetGreaterThanZero();
_stopLossPoints = Param(nameof(StopLossPoints), 90)
.SetDisplay("Stop-Loss Points", "Stop-loss distance converted through the instrument price step", "Risk Management")
.SetGreaterThanZero()
.SetOptimize(30, 150, 10);
_takeProfitPoints = Param(nameof(TakeProfitPoints), 20)
.SetDisplay("Take-Profit Points", "Take-profit distance converted through the instrument price step", "Risk Management")
.SetGreaterThanZero()
.SetOptimize(10, 80, 10);
_useStopMultiplier = Param(nameof(UseStopMultiplier), true)
.SetDisplay("Use Stop Multiplier", "Multiply distances by ten to reproduce MetaTrader stop handling", "Risk Management");
_sarAccelerationStep = Param(nameof(SarAccelerationStep), 0.02m)
.SetDisplay("SAR Step", "Initial acceleration factor for Parabolic SAR", "Indicator")
.SetRange(0.01m, 0.05m)
.SetOptimize(0.01m, 0.05m, 0.01m);
_sarAccelerationMax = Param(nameof(SarAccelerationMax), 0.2m)
.SetDisplay("SAR Max", "Maximum acceleration factor for Parabolic SAR", "Indicator")
.SetRange(0.1m, 0.4m)
.SetOptimize(0.1m, 0.4m, 0.05m);
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Candle type used for calculations", "General");
Volume = _tradeVolume.Value;
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_prevIsSarAbovePrice = null;
_longStop = null;
_longTake = null;
_shortStop = null;
_shortTake = null;
Volume = _tradeVolume.Value;
_priceStep = 0;
_lastTradeTime = default;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_priceStep = GetPriceStep();
var parabolicSar = new ParabolicSar
{
Acceleration = SarAccelerationStep,
AccelerationMax = SarAccelerationMax
};
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(parabolicSar, ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, parabolicSar);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle, decimal sarValue)
{
// Work only with completed candles to match MetaTrader logic.
if (candle.State != CandleStates.Finished)
return;
// Wait for Parabolic SAR indicator to be ready.
// Check whether existing positions should be closed by protective levels.
CheckProtectiveLevels(candle);
var isSarAbovePrice = sarValue > candle.ClosePrice;
// Initialize state on the first value.
if (_prevIsSarAbovePrice == null)
{
_prevIsSarAbovePrice = isSarAbovePrice;
return;
}
var sarSwitchedBelow = _prevIsSarAbovePrice.Value && !isSarAbovePrice;
var sarSwitchedAbove = !_prevIsSarAbovePrice.Value && isSarAbovePrice;
if (sarSwitchedBelow)
TryEnterLong(candle, sarValue);
else if (sarSwitchedAbove)
TryEnterShort(candle, sarValue);
_prevIsSarAbovePrice = isSarAbovePrice;
}
private void TryEnterLong(ICandleMessage candle, decimal sarValue)
{
// Prevent duplicate long entries.
if (Position > 0m)
return;
// Cooldown: at least 2 days between trades to avoid over-trading.
if (_lastTradeTime != default && (candle.OpenTime - _lastTradeTime).TotalHours < 48)
return;
var volume = Volume + Math.Abs(Position);
if (volume <= 0m)
return;
BuyMarket(volume);
_lastTradeTime = candle.OpenTime;
var entryPrice = candle.ClosePrice;
var stopDistance = GetDistance(StopLossPoints);
var takeDistance = GetDistance(TakeProfitPoints);
_longStop = entryPrice - stopDistance;
_longTake = entryPrice + takeDistance;
_shortStop = null;
_shortTake = null;
LogInfo($"Long entry after SAR flip. Close={entryPrice}, SAR={sarValue}, Stop={_longStop}, Take={_longTake}");
}
private void TryEnterShort(ICandleMessage candle, decimal sarValue)
{
// Prevent duplicate short entries.
if (Position < 0m)
return;
// Cooldown: at least 2 days between trades to avoid over-trading.
if (_lastTradeTime != default && (candle.OpenTime - _lastTradeTime).TotalHours < 48)
return;
var volume = Volume + Math.Abs(Position);
if (volume <= 0m)
return;
SellMarket(volume);
_lastTradeTime = candle.OpenTime;
var entryPrice = candle.ClosePrice;
var stopDistance = GetDistance(StopLossPoints);
var takeDistance = GetDistance(TakeProfitPoints);
_shortStop = entryPrice + stopDistance;
_shortTake = entryPrice - takeDistance;
_longStop = null;
_longTake = null;
LogInfo($"Short entry after SAR flip. Close={entryPrice}, SAR={sarValue}, Stop={_shortStop}, Take={_shortTake}");
}
private void CheckProtectiveLevels(ICandleMessage candle)
{
var position = Position;
if (position > 0m)
{
if (_longStop is decimal stop && candle.LowPrice <= stop)
{
SellMarket(Math.Abs(position));
LogInfo($"Long stop-loss triggered at {stop}.");
ResetLongTargets();
}
else if (_longTake is decimal take && candle.HighPrice >= take)
{
SellMarket(Math.Abs(position));
LogInfo($"Long take-profit triggered at {take}.");
ResetLongTargets();
}
}
else if (position < 0m)
{
if (_shortStop is decimal stop && candle.HighPrice >= stop)
{
BuyMarket(Math.Abs(position));
LogInfo($"Short stop-loss triggered at {stop}.");
ResetShortTargets();
}
else if (_shortTake is decimal take && candle.LowPrice <= take)
{
BuyMarket(Math.Abs(position));
LogInfo($"Short take-profit triggered at {take}.");
ResetShortTargets();
}
}
}
private void ResetLongTargets()
{
_longStop = null;
_longTake = null;
}
private void ResetShortTargets()
{
_shortStop = null;
_shortTake = null;
}
private decimal GetDistance(int basePoints)
{
var multiplier = UseStopMultiplier ? 10 : 1;
return basePoints * multiplier * _priceStep;
}
private decimal GetPriceStep()
{
// Use security price step when available, otherwise fall back to a minimal tick.
var step = Security?.PriceStep ?? 0.0001m;
return step > 0m ? step : 0.0001m;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Math, Decimal
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Indicators import ParabolicSar
from StockSharp.Algo.Strategies import Strategy
class parabolic_sar_first_dot_strategy(Strategy):
def __init__(self):
super(parabolic_sar_first_dot_strategy, self).__init__()
self._trade_volume = self.Param("TradeVolume", 0.1).SetGreaterThanZero().SetDisplay("Volume", "Order volume in lots", "General")
self._sl_points = self.Param("StopLossPoints", 90).SetGreaterThanZero().SetDisplay("Stop-Loss Points", "Stop-loss distance", "Risk")
self._tp_points = self.Param("TakeProfitPoints", 20).SetGreaterThanZero().SetDisplay("Take-Profit Points", "Take-profit distance", "Risk")
self._use_multiplier = self.Param("UseStopMultiplier", True).SetDisplay("Use Stop Multiplier", "Multiply distances by 10", "Risk")
self._sar_step = self.Param("SarAccelerationStep", 0.02).SetDisplay("SAR Step", "Initial acceleration factor", "Indicator")
self._sar_max = self.Param("SarAccelerationMax", 0.2).SetDisplay("SAR Max", "Maximum acceleration factor", "Indicator")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))).SetDisplay("Candle Type", "Candle type", "General")
@property
def CandleType(self): return self._candle_type.Value
@CandleType.setter
def CandleType(self, value): self._candle_type.Value = value
def OnReseted(self):
super(parabolic_sar_first_dot_strategy, self).OnReseted()
self._prev_is_sar_above = None
self._long_stop = None
self._long_take = None
self._short_stop = None
self._short_take = None
def OnStarted2(self, time):
super(parabolic_sar_first_dot_strategy, self).OnStarted2(time)
self._prev_is_sar_above = None
self._long_stop = None
self._long_take = None
self._short_stop = None
self._short_take = None
self._price_step = self._get_price_step()
self.Volume = self._trade_volume.Value
sar = ParabolicSar()
sar.Acceleration = self._sar_step.Value
sar.AccelerationMax = self._sar_max.Value
sub = self.SubscribeCandles(self.CandleType)
sub.Bind(sar, self.OnProcess).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, sub)
self.DrawIndicator(area, sar)
self.DrawOwnTrades(area)
def _get_price_step(self):
step = Decimal(0.0001)
if self.Security is not None and self.Security.PriceStep is not None and self.Security.PriceStep > Decimal.Zero:
step = self.Security.PriceStep
return step
def _get_distance(self, base_points):
multiplier = 10 if self._use_multiplier.Value else 1
return Decimal(base_points * multiplier) * self._price_step
def OnProcess(self, candle, sar_value):
if candle.State != CandleStates.Finished:
return
self._check_protective_levels(candle)
close = float(candle.ClosePrice)
sar_f = float(sar_value)
is_sar_above = sar_f > close
if self._prev_is_sar_above is None:
self._prev_is_sar_above = is_sar_above
return
sar_switched_below = self._prev_is_sar_above and not is_sar_above
sar_switched_above = not self._prev_is_sar_above and is_sar_above
if sar_switched_below:
self._try_enter_long(candle)
elif sar_switched_above:
self._try_enter_short(candle)
self._prev_is_sar_above = is_sar_above
def _try_enter_long(self, candle):
if self.Position > 0:
return
volume = self.Volume + Math.Abs(self.Position)
if volume <= 0:
return
self.BuyMarket(volume)
entry = candle.ClosePrice
stop_dist = self._get_distance(self._sl_points.Value)
take_dist = self._get_distance(self._tp_points.Value)
self._long_stop = entry - stop_dist
self._long_take = entry + take_dist
self._short_stop = None
self._short_take = None
def _try_enter_short(self, candle):
if self.Position < 0:
return
volume = self.Volume + Math.Abs(self.Position)
if volume <= 0:
return
self.SellMarket(volume)
entry = candle.ClosePrice
stop_dist = self._get_distance(self._sl_points.Value)
take_dist = self._get_distance(self._tp_points.Value)
self._short_stop = entry + stop_dist
self._short_take = entry - take_dist
self._long_stop = None
self._long_take = None
def _check_protective_levels(self, candle):
if self.Position > 0:
if self._long_stop is not None and candle.LowPrice <= self._long_stop:
self.SellMarket(Math.Abs(self.Position))
self._long_stop = None
self._long_take = None
elif self._long_take is not None and candle.HighPrice >= self._long_take:
self.SellMarket(Math.Abs(self.Position))
self._long_stop = None
self._long_take = None
elif self.Position < 0:
if self._short_stop is not None and candle.HighPrice >= self._short_stop:
self.BuyMarket(Math.Abs(self.Position))
self._short_stop = None
self._short_take = None
elif self._short_take is not None and candle.LowPrice <= self._short_take:
self.BuyMarket(Math.Abs(self.Position))
self._short_stop = None
self._short_take = None
def CreateClone(self):
return parabolic_sar_first_dot_strategy()