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>
/// Nova strategy converted from MQL that compares the current price with the price N seconds ago.
/// Opens a long position when the previous candle is bullish and the ask price has moved up by a threshold.
/// Opens a short position when the previous candle is bearish and the bid price has dropped below the stored ask price.
/// After a stop-loss the position size is multiplied by a coefficient, after a take-profit it resets to the base volume.
/// </summary>
public class NovaStrategy : Strategy
{
private readonly StrategyParam<int> _secondsAgo;
private readonly StrategyParam<int> _stepPips;
private readonly StrategyParam<decimal> _baseVolume;
private readonly StrategyParam<int> _stopLossPips;
private readonly StrategyParam<int> _takeProfitPips;
private readonly StrategyParam<decimal> _lossCoefficient;
private readonly StrategyParam<DataType> _candleType;
private decimal? _referenceAsk;
private decimal? _referenceBid;
private DateTimeOffset? _lastCheckTime;
private decimal _stepOffset;
private decimal _stopLossOffset;
private decimal _takeProfitOffset;
private decimal _currentVolume;
private decimal? _lastTradeVolume;
private decimal _previousPnL;
private decimal? _currentAsk;
private decimal? _currentBid;
private bool _hasPreviousCandle;
private decimal _prevCandleOpen;
private decimal _prevCandleClose;
/// <summary>
/// Seconds to look back for the price comparison.
/// </summary>
public int SecondsAgo
{
get => _secondsAgo.Value;
set => _secondsAgo.Value = value;
}
/// <summary>
/// Step in pips that is required for the breakout condition.
/// </summary>
public int StepPips
{
get => _stepPips.Value;
set => _stepPips.Value = value;
}
/// <summary>
/// Base trading volume.
/// </summary>
public decimal BaseVolume
{
get => _baseVolume.Value;
set => _baseVolume.Value = value;
}
/// <summary>
/// Stop-loss distance in pips.
/// </summary>
public int StopLossPips
{
get => _stopLossPips.Value;
set => _stopLossPips.Value = value;
}
/// <summary>
/// Take-profit distance in pips.
/// </summary>
public int TakeProfitPips
{
get => _takeProfitPips.Value;
set => _takeProfitPips.Value = value;
}
/// <summary>
/// Coefficient used to increase the volume after a stop-loss.
/// </summary>
public decimal LossCoefficient
{
get => _lossCoefficient.Value;
set => _lossCoefficient.Value = value;
}
/// <summary>
/// Candle type used for signal generation.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initializes strategy parameters.
/// </summary>
public NovaStrategy()
{
_secondsAgo = Param(nameof(SecondsAgo), 10)
.SetGreaterThanZero()
.SetDisplay("Seconds window", "Seconds to look back for price comparison", "General")
.SetOptimize(5, 30, 5);
_stepPips = Param(nameof(StepPips), 1)
.SetNotNegative()
.SetDisplay("Step (pips)", "Price offset in pips for breakout check", "Signals")
.SetOptimize(0, 5, 1);
_baseVolume = Param(nameof(BaseVolume), 1m)
.SetGreaterThanZero()
.SetDisplay("Base volume", "Initial order volume", "Risk")
.SetOptimize(0.05m, 0.5m, 0.05m);
_stopLossPips = Param(nameof(StopLossPips), 500)
.SetNotNegative()
.SetDisplay("Stop-loss (pips)", "Stop-loss distance in pips", "Risk")
.SetOptimize(0, 5, 1);
_takeProfitPips = Param(nameof(TakeProfitPips), 500)
.SetNotNegative()
.SetDisplay("Take-profit (pips)", "Take-profit distance in pips", "Risk")
.SetOptimize(0, 5, 1);
_lossCoefficient = Param(nameof(LossCoefficient), 1.6m)
.SetGreaterThanZero()
.SetDisplay("Loss coefficient", "Multiplier for the next trade after a stop-loss", "Risk")
.SetOptimize(1m, 2.5m, 0.1m);
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
.SetDisplay("Candle type", "Candles used for signal calculations", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_referenceAsk = null;
_referenceBid = null;
_lastCheckTime = null;
_stepOffset = 0m;
_stopLossOffset = 0m;
_takeProfitOffset = 0m;
_currentVolume = 0m;
_lastTradeVolume = null;
_previousPnL = 0m;
_currentAsk = null;
_currentBid = null;
_hasPreviousCandle = false;
_prevCandleOpen = 0m;
_prevCandleClose = 0m;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_previousPnL = PnL;
var pipSize = GetPipSize();
_stepOffset = StepPips * pipSize;
_stopLossOffset = StopLossPips * pipSize;
_takeProfitOffset = TakeProfitPips * pipSize;
_currentVolume = NormalizeVolume(BaseVolume);
Volume = _currentVolume;
var candleSubscription = SubscribeCandles(CandleType);
candleSubscription
.Bind(ProcessCandle)
.Start();
if (StopLossPips > 0 || TakeProfitPips > 0)
{
StartProtection(
TakeProfitPips > 0 ? new Unit(_takeProfitOffset, UnitTypes.Absolute) : default,
StopLossPips > 0 ? new Unit(_stopLossOffset, UnitTypes.Absolute) : default);
}
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, candleSubscription);
DrawOwnTrades(area);
}
}
private void ProcessLevel1(Level1ChangeMessage level1)
{
if (level1.Changes.TryGetValue(Level1Fields.BestAskPrice, out var ask))
_currentAsk = (decimal)ask;
if (level1.Changes.TryGetValue(Level1Fields.BestBidPrice, out var bid))
_currentBid = (decimal)bid;
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
UpdateVolumeFromPnL();
if (!_hasPreviousCandle)
{
_hasPreviousCandle = true;
_prevCandleOpen = candle.OpenPrice;
_prevCandleClose = candle.ClosePrice;
return;
}
if (Position != 0)
return;
var now = candle.CloseTime;
var interval = TimeSpan.FromSeconds(SecondsAgo);
if (_lastCheckTime != null && now - _lastCheckTime < interval)
return;
var currentAsk = candle.ClosePrice;
var currentBid = candle.ClosePrice;
if (currentAsk == 0m || currentBid == 0m)
{
_referenceAsk = null;
_referenceBid = null;
_lastCheckTime = now;
return;
}
if (_referenceAsk is null || _referenceBid is null)
{
_referenceAsk = currentAsk;
_referenceBid = currentBid;
_lastCheckTime = now;
return;
}
var bullishPrevious = _prevCandleClose > _prevCandleOpen;
var bearishPrevious = _prevCandleClose < _prevCandleOpen;
var referenceAsk = _referenceAsk.Value;
if (bullishPrevious && currentAsk - _stepOffset > referenceAsk)
{
TryEnterLong(currentAsk);
}
else if (bearishPrevious && currentBid + _stepOffset < referenceAsk)
{
TryEnterShort(currentBid);
}
_referenceAsk = currentAsk;
_referenceBid = currentBid;
_lastCheckTime = now;
_prevCandleOpen = candle.OpenPrice;
_prevCandleClose = candle.ClosePrice;
}
private void TryEnterLong(decimal price)
{
if (_currentVolume <= 0m)
return;
BuyMarket();
_lastTradeVolume = _currentVolume;
LogInfo($"Open long at {price:F5} with volume {_currentVolume:F2}");
}
private void TryEnterShort(decimal price)
{
if (_currentVolume <= 0m)
return;
SellMarket();
_lastTradeVolume = _currentVolume;
LogInfo($"Open short at {price:F5} with volume {_currentVolume:F2}");
}
private void UpdateVolumeFromPnL()
{
var realizedPnL = PnL;
if (realizedPnL == _previousPnL)
return;
var delta = realizedPnL - _previousPnL;
_previousPnL = realizedPnL;
if (delta > 0m)
{
_currentVolume = NormalizeVolume(BaseVolume);
Volume = _currentVolume;
LogInfo("Reset volume after profitable trade");
}
else if (delta < 0m)
{
var referenceVolume = _lastTradeVolume ?? _currentVolume;
_currentVolume = NormalizeVolume(referenceVolume * LossCoefficient);
Volume = _currentVolume;
LogInfo($"Increase volume after loss to {_currentVolume:F2}");
}
}
private decimal NormalizeVolume(decimal volume)
{
var step = Security?.VolumeStep ?? 0m;
if (step > 0m)
{
volume = Math.Floor(volume / step) * step;
}
var min = Security?.MinVolume ?? 0m;
if (min > 0m && volume < min)
{
volume = min;
}
var max = Security?.MaxVolume ?? 0m;
if (max > 0m && volume > max)
{
volume = max;
}
return volume;
}
private decimal GetPipSize()
{
var step = Security?.PriceStep ?? 1m;
var decimals = Security?.Decimals ?? 0;
var factor = decimals is 3 or 5 ? 10m : 1m;
return step * factor;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan
from StockSharp.Messages import DataType, CandleStates, Unit, UnitTypes
from StockSharp.Algo.Strategies import Strategy
class nova_strategy(Strategy):
"""Nova: compares price vs reference N seconds ago, bullish/bearish previous candle filter, martingale on loss."""
def __init__(self):
super(nova_strategy, self).__init__()
self._seconds_ago = self.Param("SecondsAgo", 10) \
.SetGreaterThanZero() \
.SetDisplay("Seconds window", "Seconds to look back for price comparison", "General")
self._step_pips = self.Param("StepPips", 1) \
.SetDisplay("Step (pips)", "Price offset in pips for breakout check", "Signals")
self._base_volume = self.Param("BaseVolume", 1.0) \
.SetGreaterThanZero() \
.SetDisplay("Base volume", "Initial order volume", "Risk")
self._stop_loss_pips = self.Param("StopLossPips", 500) \
.SetDisplay("Stop-loss (pips)", "Stop-loss distance in pips", "Risk")
self._take_profit_pips = self.Param("TakeProfitPips", 500) \
.SetDisplay("Take-profit (pips)", "Take-profit distance in pips", "Risk")
self._loss_coefficient = self.Param("LossCoefficient", 1.6) \
.SetGreaterThanZero() \
.SetDisplay("Loss coefficient", "Multiplier for the next trade after a stop-loss", "Risk")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4))) \
.SetDisplay("Candle type", "Candles used for signal calculations", "General")
self._reference_ask = None
self._reference_bid = None
self._last_check_time = None
self._step_offset = 0.0
self._stop_loss_offset = 0.0
self._take_profit_offset = 0.0
self._current_volume = 0.0
self._last_trade_volume = None
self._previous_pnl = 0.0
self._has_previous_candle = False
self._prev_candle_open = 0.0
self._prev_candle_close = 0.0
@property
def SecondsAgo(self):
return int(self._seconds_ago.Value)
@property
def StepPips(self):
return int(self._step_pips.Value)
@property
def BaseVolume(self):
return float(self._base_volume.Value)
@property
def StopLossPips(self):
return int(self._stop_loss_pips.Value)
@property
def TakeProfitPips(self):
return int(self._take_profit_pips.Value)
@property
def LossCoefficient(self):
return float(self._loss_coefficient.Value)
@property
def CandleType(self):
return self._candle_type.Value
def _get_pip_size(self):
sec = self.Security
step = float(sec.PriceStep) if sec is not None and sec.PriceStep is not None and float(sec.PriceStep) > 0 else 1.0
decimals = int(sec.Decimals) if sec is not None and sec.Decimals is not None else 0
factor = 10.0 if (decimals == 3 or decimals == 5) else 1.0
return step * factor
def OnStarted2(self, time):
super(nova_strategy, self).OnStarted2(time)
self._previous_pnl = float(self.PnL)
pip_size = self._get_pip_size()
self._step_offset = self.StepPips * pip_size
self._stop_loss_offset = self.StopLossPips * pip_size
self._take_profit_offset = self.TakeProfitPips * pip_size
self._current_volume = self.BaseVolume
self._reference_ask = None
self._reference_bid = None
self._last_check_time = None
self._last_trade_volume = None
self._has_previous_candle = False
self._prev_candle_open = 0.0
self._prev_candle_close = 0.0
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self.process_candle).Start()
if self.StopLossPips > 0 or self.TakeProfitPips > 0:
tp = Unit(self._take_profit_offset, UnitTypes.Absolute) if self.TakeProfitPips > 0 else None
sl = Unit(self._stop_loss_offset, UnitTypes.Absolute) if self.StopLossPips > 0 else None
if tp is not None and sl is not None:
self.StartProtection(takeProfit=tp, stopLoss=sl)
elif tp is not None:
self.StartProtection(takeProfit=tp)
elif sl is not None:
self.StartProtection(stopLoss=sl)
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawOwnTrades(area)
def process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
self._update_volume_from_pnl()
if not self._has_previous_candle:
self._has_previous_candle = True
self._prev_candle_open = float(candle.OpenPrice)
self._prev_candle_close = float(candle.ClosePrice)
return
if self.Position != 0:
return
now = candle.CloseTime
interval = TimeSpan.FromSeconds(self.SecondsAgo)
if self._last_check_time is not None and now - self._last_check_time < interval:
return
current_ask = float(candle.ClosePrice)
current_bid = float(candle.ClosePrice)
if current_ask == 0 or current_bid == 0:
self._reference_ask = None
self._reference_bid = None
self._last_check_time = now
return
if self._reference_ask is None or self._reference_bid is None:
self._reference_ask = current_ask
self._reference_bid = current_bid
self._last_check_time = now
return
bullish_previous = self._prev_candle_close > self._prev_candle_open
bearish_previous = self._prev_candle_close < self._prev_candle_open
ref_ask = self._reference_ask
if bullish_previous and current_ask - self._step_offset > ref_ask:
if self._current_volume > 0:
self.BuyMarket()
self._last_trade_volume = self._current_volume
elif bearish_previous and current_bid + self._step_offset < ref_ask:
if self._current_volume > 0:
self.SellMarket()
self._last_trade_volume = self._current_volume
self._reference_ask = current_ask
self._reference_bid = current_bid
self._last_check_time = now
self._prev_candle_open = float(candle.OpenPrice)
self._prev_candle_close = float(candle.ClosePrice)
def _update_volume_from_pnl(self):
realized_pnl = float(self.PnL)
if realized_pnl == self._previous_pnl:
return
delta = realized_pnl - self._previous_pnl
self._previous_pnl = realized_pnl
if delta > 0:
self._current_volume = self.BaseVolume
elif delta < 0:
ref_volume = self._last_trade_volume if self._last_trade_volume is not None else self._current_volume
self._current_volume = ref_volume * self.LossCoefficient
def OnReseted(self):
super(nova_strategy, self).OnReseted()
self._reference_ask = None
self._reference_bid = None
self._last_check_time = None
self._step_offset = 0.0
self._stop_loss_offset = 0.0
self._take_profit_offset = 0.0
self._current_volume = 0.0
self._last_trade_volume = None
self._previous_pnl = 0.0
self._has_previous_candle = False
self._prev_candle_open = 0.0
self._prev_candle_close = 0.0
def CreateClone(self):
return nova_strategy()