namespace StockSharp.Samples.Strategies;
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;
/// <summary>
/// Swap-based mean reversion strategy converted from the MetaTrader expert "Swaper 1.1".
/// Calculates a synthetic fair value using closed trades, adjusts the open position, and keeps the volume within the available margin.
/// </summary>
public class SwaperStrategy : Strategy
{
private readonly StrategyParam<decimal> _experts;
private readonly StrategyParam<decimal> _beginPrice;
private readonly StrategyParam<int> _magicNumber;
private readonly StrategyParam<decimal> _baseUnits;
private readonly StrategyParam<decimal> _contractMultiplier;
private readonly StrategyParam<decimal> _marginPerLot;
private readonly StrategyParam<decimal> _fallbackSpreadSteps;
private readonly StrategyParam<DataType> _candleType;
private decimal _initialCapital;
private decimal _realizedPnL;
private decimal _positionVolume;
private decimal _averagePrice;
private decimal? _bestBid;
private decimal? _bestAsk;
private ICandleMessage _previousCandle;
/// <summary>
/// Initializes a new instance of the <see cref="SwaperStrategy"/> class.
/// </summary>
public SwaperStrategy()
{
_experts = Param(nameof(Experts), 1m)
.SetGreaterThanZero()
.SetDisplay("Experts", "Weighting coefficient applied to the synthetic fair value.", "General");
_beginPrice = Param(nameof(BeginPrice), 1.8014m)
.SetGreaterThanZero()
.SetDisplay("Begin Price", "Initial price used to recreate the historical balance.", "General");
_magicNumber = Param(nameof(MagicNumber), 777)
.SetDisplay("Magic Number", "Identifier kept for compatibility with the MetaTrader expert.", "General");
_baseUnits = Param(nameof(BaseUnits), 1000m)
.SetGreaterThanZero()
.SetDisplay("Base Units", "Synthetic account units used when calculating the fair value denominator.", "Money Management");
_contractMultiplier = Param(nameof(ContractMultiplier), 10m)
.SetGreaterThanZero()
.SetDisplay("Contract Multiplier", "Value multiplier applied to realized and unrealized profit.", "Money Management");
_marginPerLot = Param(nameof(MarginPerLot), 1000m)
.SetGreaterThanZero()
.SetDisplay("Margin Per Lot", "Approximate capital required to keep one lot open.", "Money Management");
_fallbackSpreadSteps = Param(nameof(FallbackSpreadSteps), 1m)
.SetGreaterThanZero()
.SetDisplay("Fallback Spread (steps)", "Spread expressed in price steps when level-one data is unavailable.", "General");
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
.SetDisplay("Candle Type", "Primary timeframe that replaces the tick-based loop of the original expert.", "Data");
}
/// <summary>
/// Weighting coefficient applied to the synthetic fair value.
/// </summary>
public decimal Experts
{
get => _experts.Value;
set => _experts.Value = value;
}
/// <summary>
/// Initial price used to recreate the historical balance.
/// </summary>
public decimal BeginPrice
{
get => _beginPrice.Value;
set => _beginPrice.Value = value;
}
/// <summary>
/// Identifier kept for compatibility with the MetaTrader expert.
/// </summary>
public int MagicNumber
{
get => _magicNumber.Value;
set => _magicNumber.Value = value;
}
/// <summary>
/// Synthetic account units used when calculating the fair value denominator.
/// </summary>
public decimal BaseUnits
{
get => _baseUnits.Value;
set => _baseUnits.Value = value;
}
/// <summary>
/// Value multiplier applied to realized and unrealized profit.
/// </summary>
public decimal ContractMultiplier
{
get => _contractMultiplier.Value;
set => _contractMultiplier.Value = value;
}
/// <summary>
/// Approximate capital required to keep one lot open.
/// </summary>
public decimal MarginPerLot
{
get => _marginPerLot.Value;
set => _marginPerLot.Value = value;
}
/// <summary>
/// Spread expressed in price steps when level-one data is unavailable.
/// </summary>
public decimal FallbackSpreadSteps
{
get => _fallbackSpreadSteps.Value;
set => _fallbackSpreadSteps.Value = value;
}
/// <summary>
/// Primary timeframe that replaces the tick-based loop of the original expert.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
=> [(Security, CandleType)];
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_initialCapital = 0m;
_realizedPnL = 0m;
_positionVolume = 0m;
_averagePrice = 0m;
_bestBid = null;
_bestAsk = null;
_previousCandle = null;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_initialCapital = BaseUnits * BeginPrice;
_realizedPnL = 0m;
_positionVolume = 0m;
_averagePrice = 0m;
_bestBid = null;
_bestAsk = null;
_previousCandle = null;
var candleSubscription = SubscribeCandles(CandleType);
candleSubscription.Bind(ProcessCandle).Start();
}
private void ProcessLevel1(Level1ChangeMessage message)
{
if (message.TryGetDecimal(Level1Fields.BestBidPrice) is decimal bid)
_bestBid = bid;
if (message.TryGetDecimal(Level1Fields.BestAskPrice) is decimal ask)
_bestAsk = ask;
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
if (_previousCandle == null)
{
_previousCandle = candle;
return;
}
var security = Security;
var priceStep = security?.PriceStep ?? 0.0001m;
var spread = GetSpread(priceStep);
var high = Math.Max(candle.HighPrice, _previousCandle.HighPrice);
var low = Math.Min(candle.LowPrice, _previousCandle.LowPrice);
if (high <= 0m || low <= 0m)
{
_previousCandle = candle;
return;
}
var denominator = high + spread;
if (denominator <= 0m)
{
_previousCandle = candle;
return;
}
var com = CalculateDenominator();
if (com == 0m)
{
_previousCandle = candle;
return;
}
var money = CalculateSyntheticCapital(candle.ClosePrice);
var expertsWeight = Experts;
var dt = (money / denominator - com) * expertsWeight / (expertsWeight + 1m);
if (dt < 0m)
{
var altDenominator = money / low;
var dtAlt = (com - altDenominator) * expertsWeight / (expertsWeight + 1m);
if (dtAlt < 1m)
{
ClosePositionIfExists();
_previousCandle = candle;
return;
}
var lots = (decimal)Math.Floor((double)dtAlt) / 10m;
AdjustShort(lots);
}
else
{
if (dt < 1m)
{
ClosePositionIfExists();
_previousCandle = candle;
return;
}
var lots = (decimal)Math.Floor((double)dt) / 10m;
AdjustLong(lots);
}
_previousCandle = candle;
}
private decimal CalculateSyntheticCapital(decimal currentPrice)
{
var multiplier = ContractMultiplier;
var unrealized = _positionVolume * currentPrice * multiplier;
return _initialCapital + _realizedPnL + unrealized;
}
private decimal CalculateDenominator()
{
return BaseUnits + ContractMultiplier * _positionVolume;
}
private decimal GetSpread(decimal priceStep)
{
if (_bestBid is decimal bid && _bestAsk is decimal ask && ask > bid)
return ask - bid;
var steps = FallbackSpreadSteps;
return (steps <= 0m ? 1m : steps) * priceStep;
}
private void AdjustShort(decimal targetLots)
{
if (targetLots <= 0m)
return;
if (Position > 0m)
{
var reduce = Math.Min(Position, targetLots);
if (reduce > 0m)
SellMarket(reduce);
return;
}
var currentShort = Position < 0m ? Math.Abs(Position) : 0m;
if (currentShort >= targetLots)
return;
var additional = targetLots - currentShort;
var tradable = GetTradableVolume(additional);
if (tradable > 0m)
SellMarket(tradable);
}
private void AdjustLong(decimal targetLots)
{
if (targetLots <= 0m)
return;
if (Position < 0m)
{
var reduce = Math.Min(Math.Abs(Position), targetLots);
if (reduce > 0m)
BuyMarket(reduce);
return;
}
var currentLong = Position > 0m ? Position : 0m;
if (currentLong >= targetLots)
return;
var additional = targetLots - currentLong;
var tradable = GetTradableVolume(additional);
if (tradable > 0m)
BuyMarket(tradable);
}
private void ClosePositionIfExists()
{
var volume = Math.Abs(Position);
if (volume <= 0m)
return;
if (Position > 0m)
SellMarket(volume);
else
BuyMarket(volume);
}
private decimal GetTradableVolume(decimal desiredLots)
{
if (desiredLots <= 0m)
return 0m;
var marginPerLot = MarginPerLot;
var availableCapital = Portfolio?.CurrentValue ?? (_initialCapital + _realizedPnL);
if (marginPerLot <= 0m || availableCapital <= 0m)
return (decimal)Math.Floor((double)(desiredLots * 10m)) / 10m;
var maxLots = (decimal)Math.Floor((double)((availableCapital / marginPerLot) * 10m)) / 10m;
if (maxLots <= 0m)
return 0m;
return Math.Min(desiredLots, (decimal)maxLots);
}
/// <inheritdoc />
protected override void OnOwnTradeReceived(MyTrade trade)
{
base.OnOwnTradeReceived(trade);
var order = trade.Order;
if (order == null || order.Security != Security)
return;
var tradeInfo = trade.Trade;
var volume = tradeInfo.Volume;
if (volume <= 0m)
return;
var signedVolume = order.Side == Sides.Buy ? volume : -volume;
var price = tradeInfo.Price;
if (_positionVolume == 0m || Math.Sign(_positionVolume) == Math.Sign(signedVolume))
{
var totalVolume = _positionVolume + signedVolume;
if (totalVolume == 0m)
{
_positionVolume = 0m;
_averagePrice = 0m;
}
else
{
var weightedPrice = _averagePrice * _positionVolume + price * signedVolume;
_positionVolume = totalVolume;
_averagePrice = weightedPrice / totalVolume;
}
return;
}
var closingVolume = Math.Min(Math.Abs(signedVolume), Math.Abs(_positionVolume));
var realized = (price - _averagePrice) * closingVolume * Math.Sign(_positionVolume) * ContractMultiplier;
_realizedPnL += realized;
var remainingVolume = _positionVolume + signedVolume;
if (remainingVolume == 0m)
{
_positionVolume = 0m;
_averagePrice = 0m;
return;
}
if (Math.Sign(_positionVolume) == Math.Sign(remainingVolume))
{
_positionVolume = remainingVolume;
return;
}
_positionVolume = remainingVolume;
_averagePrice = price;
}
}
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
from StockSharp.Algo.Strategies import Strategy
class swaper_strategy(Strategy):
"""Swap-based mean reversion strategy. Calculates a synthetic fair value using
realized PnL and adjusts position volume based on the deviation from that value."""
def __init__(self):
super(swaper_strategy, self).__init__()
self._experts = self.Param("Experts", 1.0) \
.SetGreaterThanZero() \
.SetDisplay("Experts", "Weighting coefficient applied to the synthetic fair value", "General")
self._begin_price = self.Param("BeginPrice", 1.8014) \
.SetGreaterThanZero() \
.SetDisplay("Begin Price", "Initial price used to recreate the historical balance", "General")
self._magic_number = self.Param("MagicNumber", 777) \
.SetDisplay("Magic Number", "Identifier kept for compatibility with the MetaTrader expert", "General")
self._base_units = self.Param("BaseUnits", 1000.0) \
.SetGreaterThanZero() \
.SetDisplay("Base Units", "Synthetic account units used when calculating the fair value denominator", "Money Management")
self._contract_multiplier = self.Param("ContractMultiplier", 10.0) \
.SetGreaterThanZero() \
.SetDisplay("Contract Multiplier", "Value multiplier applied to realized and unrealized profit", "Money Management")
self._margin_per_lot = self.Param("MarginPerLot", 1000.0) \
.SetGreaterThanZero() \
.SetDisplay("Margin Per Lot", "Approximate capital required to keep one lot open", "Money Management")
self._fallback_spread_steps = self.Param("FallbackSpreadSteps", 1.0) \
.SetGreaterThanZero() \
.SetDisplay("Fallback Spread (steps)", "Spread expressed in price steps when level-one data is unavailable", "General")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(1))) \
.SetDisplay("Candle Type", "Primary timeframe that replaces the tick-based loop of the original expert", "Data")
self._initial_capital = 0.0
self._realized_pnl = 0.0
self._position_volume = 0.0
self._average_price = 0.0
self._previous_candle = None
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
@property
def Experts(self):
return self._experts.Value
@property
def BeginPrice(self):
return self._begin_price.Value
@property
def BaseUnits(self):
return self._base_units.Value
@property
def ContractMultiplier(self):
return self._contract_multiplier.Value
@property
def MarginPerLot(self):
return self._margin_per_lot.Value
@property
def FallbackSpreadSteps(self):
return self._fallback_spread_steps.Value
def OnReseted(self):
super(swaper_strategy, self).OnReseted()
self._initial_capital = 0.0
self._realized_pnl = 0.0
self._position_volume = 0.0
self._average_price = 0.0
self._previous_candle = None
def OnStarted2(self, time):
super(swaper_strategy, self).OnStarted2(time)
self._initial_capital = float(self.BaseUnits) * float(self.BeginPrice)
self._realized_pnl = 0.0
self._position_volume = 0.0
self._average_price = 0.0
self._previous_candle = None
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self._process_candle).Start()
def _process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
if self._previous_candle is None:
self._previous_candle = candle
return
price_step = 0.0001
if self.Security is not None and self.Security.PriceStep is not None:
ps = float(self.Security.PriceStep)
if ps > 0:
price_step = ps
spread = self._get_spread(price_step)
high = max(float(candle.HighPrice), float(self._previous_candle.HighPrice))
low = min(float(candle.LowPrice), float(self._previous_candle.LowPrice))
if high <= 0 or low <= 0:
self._previous_candle = candle
return
denominator = high + spread
if denominator <= 0:
self._previous_candle = candle
return
com = self._calculate_denominator()
if com == 0:
self._previous_candle = candle
return
close_price = float(candle.ClosePrice)
money = self._calculate_synthetic_capital(close_price)
experts_weight = float(self.Experts)
dt = (money / denominator - com) * experts_weight / (experts_weight + 1.0)
if dt < 0:
alt_denominator = money / low if low > 0 else 0
if alt_denominator == 0:
self._previous_candle = candle
return
dt_alt = (com - alt_denominator) * experts_weight / (experts_weight + 1.0)
if dt_alt < 1.0:
self._close_position_if_exists()
self._previous_candle = candle
return
lots = Math.Floor(dt_alt) / 10.0
self._adjust_short(lots, close_price)
else:
if dt < 1.0:
self._close_position_if_exists()
self._previous_candle = candle
return
lots = Math.Floor(dt) / 10.0
self._adjust_long(lots, close_price)
self._previous_candle = candle
def _calculate_synthetic_capital(self, current_price):
multiplier = float(self.ContractMultiplier)
unrealized = self._position_volume * current_price * multiplier
return self._initial_capital + self._realized_pnl + unrealized
def _calculate_denominator(self):
return float(self.BaseUnits) + float(self.ContractMultiplier) * self._position_volume
def _get_spread(self, price_step):
steps = float(self.FallbackSpreadSteps)
if steps <= 0:
steps = 1.0
return steps * price_step
def _adjust_short(self, target_lots, current_price):
if target_lots <= 0:
return
if self.Position > 0:
reduce = min(float(self.Position), target_lots)
if reduce > 0:
self.SellMarket(reduce)
self._update_position_tracking(-reduce, current_price)
return
current_short = abs(float(self.Position)) if self.Position < 0 else 0.0
if current_short >= target_lots:
return
additional = target_lots - current_short
tradable = self._get_tradable_volume(additional)
if tradable > 0:
self.SellMarket(tradable)
self._update_position_tracking(-tradable, current_price)
def _adjust_long(self, target_lots, current_price):
if target_lots <= 0:
return
if self.Position < 0:
reduce = min(abs(float(self.Position)), target_lots)
if reduce > 0:
self.BuyMarket(reduce)
self._update_position_tracking(reduce, current_price)
return
current_long = float(self.Position) if self.Position > 0 else 0.0
if current_long >= target_lots:
return
additional = target_lots - current_long
tradable = self._get_tradable_volume(additional)
if tradable > 0:
self.BuyMarket(tradable)
self._update_position_tracking(tradable, current_price)
def _close_position_if_exists(self):
volume = abs(float(self.Position))
if volume <= 0:
return
if self.Position > 0:
self.SellMarket(volume)
else:
self.BuyMarket(volume)
self._position_volume = 0.0
self._average_price = 0.0
def _get_tradable_volume(self, desired_lots):
if desired_lots <= 0:
return 0.0
margin_per_lot = float(self.MarginPerLot)
available_capital = self._initial_capital + self._realized_pnl
if margin_per_lot <= 0 or available_capital <= 0:
return Math.Floor(desired_lots * 10.0) / 10.0
max_lots = Math.Floor(available_capital / margin_per_lot * 10.0) / 10.0
if max_lots <= 0:
return 0.0
return min(desired_lots, max_lots)
def _update_position_tracking(self, signed_volume, price):
if self._position_volume == 0 or \
(self._position_volume > 0 and signed_volume > 0) or \
(self._position_volume < 0 and signed_volume < 0):
total_volume = self._position_volume + signed_volume
if total_volume == 0:
self._position_volume = 0.0
self._average_price = 0.0
else:
weighted = self._average_price * self._position_volume + price * signed_volume
self._position_volume = total_volume
self._average_price = weighted / total_volume
return
closing_volume = min(abs(signed_volume), abs(self._position_volume))
sign = 1.0 if self._position_volume > 0 else -1.0
realized = (price - self._average_price) * closing_volume * sign * float(self.ContractMultiplier)
self._realized_pnl += realized
remaining = self._position_volume + signed_volume
if remaining == 0:
self._position_volume = 0.0
self._average_price = 0.0
return
if (self._position_volume > 0 and remaining > 0) or (self._position_volume < 0 and remaining < 0):
self._position_volume = remaining
return
self._position_volume = remaining
self._average_price = price
def CreateClone(self):
return swaper_strategy()