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>
/// Port of the MetaTrader strategy open_close2ampnstochastic_strategy.
/// Replicates the price-action filters combined with a Stochastic crossover and includes the original money management rules.
/// </summary>
public class OpenClose2AmpnStochasticStrategy : Strategy
{
private readonly StrategyParam<decimal> _baseVolume;
private readonly StrategyParam<decimal> _maximumRisk;
private readonly StrategyParam<decimal> _decreaseFactor;
private readonly StrategyParam<decimal> _minimumVolume;
private readonly StrategyParam<int> _stochasticLength;
private readonly StrategyParam<int> _stochasticKLength;
private readonly StrategyParam<int> _stochasticDLength;
private readonly StrategyParam<DataType> _candleType;
private StochasticOscillator _stochastic = null!;
private decimal? _previousOpen;
private decimal? _previousClose;
private decimal _averageEntryPrice;
private decimal _entryVolume;
private int _entryDirection;
private int _lossStreak;
/// <summary>
/// Base position size used when portfolio data is unavailable.
/// </summary>
public decimal BaseVolume
{
get => _baseVolume.Value;
set => _baseVolume.Value = value;
}
/// <summary>
/// Fraction of account value used for risk sizing and the drawdown guard.
/// </summary>
public decimal MaximumRisk
{
get => _maximumRisk.Value;
set => _maximumRisk.Value = value;
}
/// <summary>
/// Scaling factor that reduces position size after consecutive losing trades.
/// </summary>
public decimal DecreaseFactor
{
get => _decreaseFactor.Value;
set => _decreaseFactor.Value = value;
}
/// <summary>
/// Minimum tradable volume that mirrors the original 0.1 lot floor.
/// </summary>
public decimal MinimumVolume
{
get => _minimumVolume.Value;
set => _minimumVolume.Value = value;
}
/// <summary>
/// Stochastic oscillator look-back period.
/// </summary>
public int StochasticLength
{
get => _stochasticLength.Value;
set => _stochasticLength.Value = value;
}
/// <summary>
/// %K smoothing period of the Stochastic oscillator.
/// </summary>
public int StochasticKLength
{
get => _stochasticKLength.Value;
set => _stochasticKLength.Value = value;
}
/// <summary>
/// %D smoothing period of the Stochastic oscillator.
/// </summary>
public int StochasticDLength
{
get => _stochasticDLength.Value;
set => _stochasticDLength.Value = value;
}
/// <summary>
/// Candle type used for indicator calculations.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initializes strategy parameters.
/// </summary>
public OpenClose2AmpnStochasticStrategy()
{
_baseVolume = Param(nameof(BaseVolume), 0.1m)
.SetGreaterThanZero()
.SetDisplay("Base Volume", "Fallback order volume when risk sizing is unavailable", "Money Management");
_maximumRisk = Param(nameof(MaximumRisk), 0.3m)
.SetNotNegative()
.SetDisplay("Maximum Risk", "Fraction of equity used for sizing and the drawdown guard", "Money Management");
_decreaseFactor = Param(nameof(DecreaseFactor), 100m)
.SetNotNegative()
.SetDisplay("Decrease Factor", "Divisor applied after losing trades to shrink the next position", "Money Management");
_minimumVolume = Param(nameof(MinimumVolume), 0.1m)
.SetGreaterThanZero()
.SetDisplay("Minimum Volume", "Lowest volume allowed after money management adjustments", "Money Management");
_stochasticLength = Param(nameof(StochasticLength), 9)
.SetGreaterThanZero()
.SetDisplay("Stochastic Length", "Number of periods used by the Stochastic oscillator", "Indicators");
_stochasticKLength = Param(nameof(StochasticKLength), 3)
.SetGreaterThanZero()
.SetDisplay("Stochastic %K", "Smoothing applied to the %K line", "Indicators");
_stochasticDLength = Param(nameof(StochasticDLength), 3)
.SetGreaterThanZero()
.SetDisplay("Stochastic %D", "Smoothing applied to the %D signal line", "Indicators");
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
.SetDisplay("Candle Type", "Time-frame used for processing", "General");
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_previousOpen = null;
_previousClose = null;
_averageEntryPrice = 0m;
_entryVolume = 0m;
_entryDirection = 0;
_lossStreak = 0;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
// Build the Stochastic oscillator that mirrors the original (9,3,3) setup.
_stochastic = new StochasticOscillator
{
K = { Length = StochasticKLength },
D = { Length = StochasticDLength },
};
// Subscribe to candle data and bind the indicator values.
var subscription = SubscribeCandles(CandleType);
subscription
.BindEx(_stochastic, ProcessCandle)
.Start();
// Draw indicator data if a chart is available.
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, _stochastic);
DrawOwnTrades(area);
}
// Enable built-in protection helpers (stop orders, etc.).
StartProtection(null, null);
}
private void ProcessCandle(ICandleMessage candle, IIndicatorValue stochasticValue)
{
// Process signals only once per finished candle.
if (candle.State != CandleStates.Finished)
return;
if (!stochasticValue.IsFinal)
return;
var stochastic = (StochasticOscillatorValue)stochasticValue;
if (stochastic.K is not decimal main || stochastic.D is not decimal signal)
return;
// Evaluate the emergency drawdown guard before new signals.
if (Position != 0m && ApplyRiskGuard(candle.ClosePrice))
{
UpdatePreviousPrices(candle);
return;
}
var previousOpen = _previousOpen;
var previousClose = _previousClose;
var canTrade = IsFormedAndOnlineAndAllowTrading();
if (previousOpen is decimal prevOpen && previousClose is decimal prevClose)
{
if (Position == 0m && canTrade)
{
var longSignal = main > signal && candle.OpenPrice < prevOpen && candle.ClosePrice < prevClose;
var shortSignal = main < signal && candle.OpenPrice > prevOpen && candle.ClosePrice > prevClose;
if (longSignal)
{
var volume = CalculateTradeVolume(candle.ClosePrice);
if (volume > 0m)
{
BuyMarket(volume);
LogInfo($"Enter long: main={main:F2}, signal={signal:F2}, open={candle.OpenPrice}, close={candle.ClosePrice}, volume={volume}");
}
}
else if (shortSignal)
{
var volume = CalculateTradeVolume(candle.ClosePrice);
if (volume > 0m)
{
SellMarket(volume);
LogInfo($"Enter short: main={main:F2}, signal={signal:F2}, open={candle.OpenPrice}, close={candle.ClosePrice}, volume={volume}");
}
}
}
else if (Position > 0m)
{
var exitLong = main < signal && candle.OpenPrice > prevOpen && candle.ClosePrice > prevClose;
if (exitLong)
{
ClosePosition(candle.ClosePrice);
LogInfo($"Exit long: main={main:F2}, signal={signal:F2}");
}
}
else if (Position < 0m)
{
var exitShort = main > signal && candle.OpenPrice < prevOpen && candle.ClosePrice < prevClose;
if (exitShort)
{
ClosePosition(candle.ClosePrice);
LogInfo($"Exit short: main={main:F2}, signal={signal:F2}");
}
}
}
UpdatePreviousPrices(candle);
}
private bool ApplyRiskGuard(decimal closePrice)
{
if (MaximumRisk <= 0m)
return false;
var floatingPnL = CalculateFloatingPnL(closePrice);
if (floatingPnL >= 0m)
return false;
var marginBase = GetMarginBase();
if (marginBase <= 0m)
return false;
var limit = marginBase * MaximumRisk;
if (Math.Abs(floatingPnL) < limit)
return false;
LogInfo($"Risk guard triggered: floatingPnL={floatingPnL}, limit={limit}. Closing position.");
ClosePosition(closePrice);
return true;
}
private decimal CalculateTradeVolume(decimal price)
{
var volume = BaseVolume;
// Derive the lot size from account value similar to the original EA.
var accountValue = Portfolio?.CurrentValue;
if (accountValue is decimal value && value > 0m && price > 0m && MaximumRisk > 0m)
{
var riskVolume = Math.Round(value * MaximumRisk / 1000m, 2, MidpointRounding.AwayFromZero);
if (riskVolume > 0m)
volume = riskVolume;
}
// Apply loss streak reduction once at least two losses occurred, matching the MT4 script.
if (DecreaseFactor > 0m && _lossStreak > 1)
{
var reduction = volume * _lossStreak / DecreaseFactor;
volume -= reduction;
}
if (volume < MinimumVolume)
volume = MinimumVolume;
return AdjustVolume(volume);
}
private decimal AdjustVolume(decimal volume)
{
if (Security is null)
return volume;
var step = Security.VolumeStep ?? 0m;
if (step > 0m)
{
var steps = Math.Floor(volume / step);
if (steps <= 0m)
steps = 1m;
volume = steps * step;
}
var minVolume = Security.MinVolume ?? 0m;
if (minVolume > 0m && volume < minVolume)
volume = minVolume;
var maxVolume = Security.MaxVolume;
if (maxVolume.HasValue && maxVolume.Value > 0m && volume > maxVolume.Value)
volume = maxVolume.Value;
return volume;
}
private void ClosePosition(decimal closePrice)
{
var volume = Math.Abs(Position);
if (volume <= 0m)
{
ResetEntryState();
return;
}
if (Position > 0m)
SellMarket(volume);
else
BuyMarket(volume);
// Estimate profit using the stored average entry price.
if (_entryDirection != 0 && _averageEntryPrice > 0m)
{
var profit = _entryDirection > 0 ? closePrice - _averageEntryPrice : _averageEntryPrice - closePrice;
if (profit < 0m)
_lossStreak++;
else if (profit > 0m)
_lossStreak = 0;
}
ResetEntryState();
}
private void UpdatePreviousPrices(ICandleMessage candle)
{
_previousOpen = candle.OpenPrice;
_previousClose = candle.ClosePrice;
}
private decimal CalculateFloatingPnL(decimal price)
{
if (Position == 0m)
return 0m;
var entryPrice = _averageEntryPrice;
if (entryPrice == 0m)
return 0m;
var priceMove = price - entryPrice;
var priceStep = Security?.PriceStep ?? 0m;
var stepPrice = GetSecurityValue<decimal?>(Level1Fields.StepPrice) ?? 0m;
if (priceStep > 0m && stepPrice > 0m)
{
var steps = priceMove / priceStep;
return steps * stepPrice * Position;
}
return priceMove * Position;
}
private decimal GetMarginBase()
{
if (Portfolio == null)
return 0m;
if (Portfolio.BlockedValue is decimal blocked && blocked > 0m)
return blocked;
if (Portfolio.CurrentValue is decimal value && value > 0m)
return value;
return 0m;
}
/// <inheritdoc />
protected override void OnOwnTradeReceived(MyTrade trade)
{
base.OnOwnTradeReceived(trade);
if (trade?.Order == null || trade.Trade == null)
return;
var price = trade.Trade.Price;
var volume = trade.Trade.Volume;
if (trade.Order.Side == Sides.Buy)
{
if (Position > 0m)
{
RegisterEntry(price, volume, 1);
}
else if (Position == 0m && _entryDirection == -1)
{
EvaluateClosedTrade(price);
}
}
else if (trade.Order.Side == Sides.Sell)
{
if (Position < 0m)
{
RegisterEntry(price, volume, -1);
}
else if (Position == 0m && _entryDirection == 1)
{
EvaluateClosedTrade(price);
}
}
}
private void RegisterEntry(decimal price, decimal volume, int direction)
{
if (volume <= 0m)
return;
if (_entryDirection != direction)
{
_entryDirection = direction;
_averageEntryPrice = price;
_entryVolume = volume;
return;
}
var totalVolume = _entryVolume + volume;
if (totalVolume <= 0m)
{
ResetEntryState();
return;
}
_averageEntryPrice = (_averageEntryPrice * _entryVolume + price * volume) / totalVolume;
_entryVolume = totalVolume;
}
private void EvaluateClosedTrade(decimal exitPrice)
{
if (_entryDirection == 0 || _averageEntryPrice <= 0m)
{
ResetEntryState();
return;
}
var profit = _entryDirection > 0 ? exitPrice - _averageEntryPrice : _averageEntryPrice - exitPrice;
if (profit < 0m)
{
_lossStreak++;
}
else if (profit > 0m)
{
_lossStreak = 0;
}
ResetEntryState();
}
private void ResetEntryState()
{
_averageEntryPrice = 0m;
_entryVolume = 0m;
_entryDirection = 0;
}
}
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
from StockSharp.Messages import DataType, CandleStates, Sides
from StockSharp.Algo.Strategies import Strategy
from StockSharp.Algo.Indicators import StochasticOscillator
class open_close2_ampn_stochastic_strategy(Strategy):
def __init__(self):
super(open_close2_ampn_stochastic_strategy, self).__init__()
self._base_volume = self.Param("BaseVolume", 0.1) \
.SetDisplay("Base Volume", "Fallback order volume when risk sizing is unavailable", "Money Management")
self._maximum_risk = self.Param("MaximumRisk", 0.3) \
.SetDisplay("Maximum Risk", "Fraction of equity used for sizing and the drawdown guard", "Money Management")
self._decrease_factor = self.Param("DecreaseFactor", 100.0) \
.SetDisplay("Decrease Factor", "Divisor applied after losing trades to shrink the next position", "Money Management")
self._minimum_volume = self.Param("MinimumVolume", 0.1) \
.SetDisplay("Minimum Volume", "Lowest volume allowed after money management adjustments", "Money Management")
self._stochastic_length = self.Param("StochasticLength", 9) \
.SetDisplay("Stochastic Length", "Number of periods used by the Stochastic oscillator", "Indicators")
self._stochastic_k_length = self.Param("StochasticKLength", 3) \
.SetDisplay("Stochastic %K", "Smoothing applied to the %K line", "Indicators")
self._stochastic_d_length = self.Param("StochasticDLength", 3) \
.SetDisplay("Stochastic %D", "Smoothing applied to the %D signal line", "Indicators")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(1))) \
.SetDisplay("Candle Type", "Time-frame used for processing", "General")
self._previous_open = None
self._previous_close = None
self._average_entry_price = 0.0
self._entry_volume = 0.0
self._entry_direction = 0
self._loss_streak = 0
@property
def BaseVolume(self):
return self._base_volume.Value
@property
def MaximumRisk(self):
return self._maximum_risk.Value
@property
def DecreaseFactor(self):
return self._decrease_factor.Value
@property
def MinimumVolume(self):
return self._minimum_volume.Value
@property
def StochasticLength(self):
return self._stochastic_length.Value
@property
def StochasticKLength(self):
return self._stochastic_k_length.Value
@property
def StochasticDLength(self):
return self._stochastic_d_length.Value
@property
def CandleType(self):
return self._candle_type.Value
def OnStarted2(self, time):
super(open_close2_ampn_stochastic_strategy, self).OnStarted2(time)
self._stochastic = StochasticOscillator()
self._stochastic.K.Length = self.StochasticKLength
self._stochastic.D.Length = self.StochasticDLength
subscription = self.SubscribeCandles(self.CandleType)
subscription.BindEx(self._stochastic, self.ProcessCandle).Start()
def ProcessCandle(self, candle, stochastic_value):
if candle.State != CandleStates.Finished:
return
if not stochastic_value.IsFinal:
return
k_val = stochastic_value.K
d_val = stochastic_value.D
if k_val is None or d_val is None:
return
main = float(k_val)
signal = float(d_val)
close_price = float(candle.ClosePrice)
open_price = float(candle.OpenPrice)
if self.Position != 0 and self._apply_risk_guard(close_price):
self._update_previous_prices(candle)
return
can_trade = self.IsFormedAndOnlineAndAllowTrading()
if self._previous_open is not None and self._previous_close is not None:
prev_open = self._previous_open
prev_close = self._previous_close
if self.Position == 0 and can_trade:
long_signal = main > signal and open_price < prev_open and close_price < prev_close
short_signal = main < signal and open_price > prev_open and close_price > prev_close
if long_signal:
volume = self._calculate_trade_volume(close_price)
if volume > 0:
self.BuyMarket(volume)
elif short_signal:
volume = self._calculate_trade_volume(close_price)
if volume > 0:
self.SellMarket(volume)
elif self.Position > 0:
exit_long = main < signal and open_price > prev_open and close_price > prev_close
if exit_long:
self._close_position(close_price)
elif self.Position < 0:
exit_short = main > signal and open_price < prev_open and close_price < prev_close
if exit_short:
self._close_position(close_price)
self._update_previous_prices(candle)
def _apply_risk_guard(self, close_price):
max_risk = float(self.MaximumRisk)
if max_risk <= 0:
return False
floating_pnl = self._calculate_floating_pnl(close_price)
if floating_pnl >= 0:
return False
margin_base = self._get_margin_base()
if margin_base <= 0:
return False
limit = margin_base * max_risk
if abs(floating_pnl) < limit:
return False
self._close_position(close_price)
return True
def _calculate_trade_volume(self, price):
volume = float(self.BaseVolume)
max_risk = float(self.MaximumRisk)
if self.Portfolio is not None:
account_value = self.Portfolio.CurrentValue
if account_value is not None and float(account_value) > 0 and price > 0 and max_risk > 0:
risk_volume = round(float(account_value) * max_risk / 1000.0, 2)
if risk_volume > 0:
volume = risk_volume
dec_factor = float(self.DecreaseFactor)
if dec_factor > 0 and self._loss_streak > 1:
reduction = volume * self._loss_streak / dec_factor
volume -= reduction
min_vol = float(self.MinimumVolume)
if volume < min_vol:
volume = min_vol
return volume
def _close_position(self, close_price):
vol = abs(self.Position)
if vol <= 0:
self._reset_entry_state()
return
if self.Position > 0:
self.SellMarket(vol)
else:
self.BuyMarket(vol)
if self._entry_direction != 0 and self._average_entry_price > 0:
profit = close_price - self._average_entry_price if self._entry_direction > 0 else self._average_entry_price - close_price
if profit < 0:
self._loss_streak += 1
elif profit > 0:
self._loss_streak = 0
self._reset_entry_state()
def _update_previous_prices(self, candle):
self._previous_open = float(candle.OpenPrice)
self._previous_close = float(candle.ClosePrice)
def _calculate_floating_pnl(self, price):
if self.Position == 0:
return 0.0
entry_price = self._average_entry_price
if entry_price == 0:
return 0.0
price_move = price - entry_price
return price_move * self.Position
def _get_margin_base(self):
if self.Portfolio is None:
return 0.0
blocked = self.Portfolio.BlockedValue
if blocked is not None and float(blocked) > 0:
return float(blocked)
current = self.Portfolio.CurrentValue
if current is not None and float(current) > 0:
return float(current)
return 0.0
def _reset_entry_state(self):
self._average_entry_price = 0.0
self._entry_volume = 0.0
self._entry_direction = 0
def _register_entry(self, price, volume, direction):
if volume <= 0:
return
if self._entry_direction != direction:
self._entry_direction = direction
self._average_entry_price = price
self._entry_volume = volume
return
total_volume = self._entry_volume + volume
if total_volume <= 0:
self._reset_entry_state()
return
self._average_entry_price = (self._average_entry_price * self._entry_volume + price * volume) / total_volume
self._entry_volume = total_volume
def OnOwnTradeReceived(self, trade):
super(open_close2_ampn_stochastic_strategy, self).OnOwnTradeReceived(trade)
if trade is None or trade.Order is None or trade.Trade is None:
return
price = float(trade.Trade.Price)
volume = float(trade.Trade.Volume)
if trade.Order.Side == Sides.Buy:
if self.Position > 0:
self._register_entry(price, volume, 1)
elif self.Position == 0 and self._entry_direction == -1:
self._evaluate_closed_trade(price)
elif trade.Order.Side == Sides.Sell:
if self.Position < 0:
self._register_entry(price, volume, -1)
elif self.Position == 0 and self._entry_direction == 1:
self._evaluate_closed_trade(price)
def _evaluate_closed_trade(self, exit_price):
if self._entry_direction == 0 or self._average_entry_price <= 0:
self._reset_entry_state()
return
profit = exit_price - self._average_entry_price if self._entry_direction > 0 else self._average_entry_price - exit_price
if profit < 0:
self._loss_streak += 1
elif profit > 0:
self._loss_streak = 0
self._reset_entry_state()
def OnReseted(self):
super(open_close2_ampn_stochastic_strategy, self).OnReseted()
self._previous_open = None
self._previous_close = None
self._average_entry_price = 0.0
self._entry_volume = 0.0
self._entry_direction = 0
self._loss_streak = 0
def CreateClone(self):
return open_close2_ampn_stochastic_strategy()