Smart Trend Follower 策略
概述
Smart Trend Follower 策略 是 MetaTrader 5 智能交易系统 Smart Trend Follower 的 StockSharp 版本。原始 EA 在反向移动平均线交叉和顺势的随机指标确认之间切换,并使用倍量网格扩大持仓。本移植版完全使用 StockSharp 高级 API(K 线订阅、指标绑定、市场委托)实现相同的交易流程,同时保留分批加仓和统一止盈/止损 管理。
信号逻辑
通过 SignalMode 参数可以选择两种信号模式:
- CrossMa – 保留原策略的“逆势交叉”逻辑。当快速 SMA 从上向下穿越慢速 SMA(当前 fast < slow,前一根 fast > slow)时建立或加仓多头;当快速 SMA 从下向上穿越慢速 SMA(当前 fast > slow,前一根 fast < slow) 时建立或加仓空头。
- Trend – 对应原策略的顺势模式。仅当 fast > slow、当前 K 线收阳且随机指标 %K ≤ 30 时触发多头;当 fast < slow、当前 K 线收阴且 %K ≥ 70 时触发空头。
所有条件仅在已完成的 K 线上评估。如果出现新信号而方向相反的持仓仍存在,策略会先用市价单平掉反向仓, 然后再根据新信号处理建仓与加仓,确保始终与当前信号方向保持一致。
网格加仓
策略按以下规则复制原 EA 的马丁加仓方式:
- 首单使用
InitialVolume指定的手数。 - 之后每次加仓的手数均乘以
Multiplier(当 ≤ 1 时视为关闭倍量)。 - 仅当价格相对当前方向的最佳入场价(多头取最低成交价,空头取最高成交价)偏移至少
LayerDistancePips点时,才允许追加同向订单。 - 下单量根据交易品种的
VolumeStep、VolumeMin、VolumeMax自动归一化。
风险控制
策略为每个方向分别维护加权平均价,并据此设置统一止盈/止损:
TakeProfitPips指定从平均价到篮子止盈价的距离。多头在 K 线最高价触及该水平时全部平仓,空头在最低价 触及时平仓;设为 0 可关闭止盈。StopLossPips以同样方式设置保护性止损。多头在最低价跌破止损时平仓,空头在最高价突破止损时平仓;设为 0 可关闭硬止损。
平仓通过下一根完成的 K 线确认达到价位后,以市场委托执行。_longExitRequested 与 _shortExitRequested
标志避免在成交回报到达前重复发送平仓指令。
参数
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
SignalMode |
枚举 (CrossMa, Trend) |
CrossMa |
选择使用逆势交叉或顺势+随机指标逻辑。 |
CandleType |
DataType |
30 分钟 | 指标与信号使用的主时间框。 |
InitialVolume |
decimal | 0.01 |
首次建仓的手数。 |
Multiplier |
decimal | 2 |
每次加仓的手数乘数。 |
LayerDistancePips |
decimal | 200 |
同向再次加仓所需的最小点差。 |
FastPeriod |
int | 14 |
快速 SMA 周期。 |
SlowPeriod |
int | 28 |
慢速 SMA 周期,必须大于 FastPeriod。 |
StochasticKPeriod |
int | 10 |
随机指标 %K 的基础周期。 |
StochasticDPeriod |
int | 3 |
%D 平滑周期。 |
StochasticSlowing |
int | 3 |
%K 额外平滑周期。 |
TakeProfitPips |
decimal | 500 |
从均价到止盈位的点差,0 表示关闭。 |
StopLossPips |
decimal | 0 |
从均价到止损位的点差,0 表示关闭。 |
实现细节
- 点值根据品种的
PriceStep与Decimals推算,匹配 MetaTrader 中的 point 定义(例如五位报价为 0.0001)。 - 使用两个
PositionEntry列表保存多头与空头篮子的逐笔成交,并在反向成交时按先进先出方式扣减。 - 指标全部通过
SubscribeCandles().BindEx(...)绑定,无需手动调用GetValue,也不会把指标直接加入Strategy.Indicators。 - 启动时调用
StartProtection(),以便使用 StockSharp 的风险保护模块(保本、风控等)。 - 为保持逻辑确定性并贴近原始 EA,当存在反向仓时会先行平仓,再处理新的同向信号。
文件
CS/SmartTrendFollowerStrategy.cs– 使用 StockSharp 高级 API 编写的 C# 策略实现。
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 "Smart Trend Follower" MetaTrader 5 expert advisor that combines moving average signals
/// with stochastic confirmation and a martingale-style layering engine.
/// </summary>
public class SmartTrendFollowerStrategy : Strategy
{
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<SignalModes> _signalMode;
private readonly StrategyParam<decimal> _initialVolume;
private readonly StrategyParam<decimal> _multiplier;
private readonly StrategyParam<decimal> _layerDistancePips;
private readonly StrategyParam<int> _fastPeriod;
private readonly StrategyParam<int> _slowPeriod;
private readonly StrategyParam<int> _stochasticKPeriod;
private readonly StrategyParam<int> _stochasticDPeriod;
private readonly StrategyParam<int> _stochasticSlowing;
private readonly StrategyParam<decimal> _takeProfitPips;
private readonly StrategyParam<decimal> _stopLossPips;
private SimpleMovingAverage _fastSma;
private SimpleMovingAverage _slowSma;
private StochasticOscillator _stochastic;
private readonly List<PositionEntry> _longEntries = new();
private readonly List<PositionEntry> _shortEntries = new();
private decimal? _prevFast;
private decimal? _prevSlow;
private decimal _pipSize;
private bool _longExitRequested;
private bool _shortExitRequested;
/// <summary>
/// Trading signal mode.
/// </summary>
public SignalModes SignalMode
{
get => _signalMode.Value;
set => _signalMode.Value = value;
}
/// <summary>
/// Base candle type used by the strategy.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initial order volume expressed in lots.
/// </summary>
public decimal InitialVolume
{
get => _initialVolume.Value;
set => _initialVolume.Value = value;
}
/// <summary>
/// Multiplier applied to the volume of every additional averaging order.
/// </summary>
public decimal Multiplier
{
get => _multiplier.Value;
set => _multiplier.Value = value;
}
/// <summary>
/// Distance in pips required before stacking another order in the same direction.
/// </summary>
public decimal LayerDistancePips
{
get => _layerDistancePips.Value;
set => _layerDistancePips.Value = value;
}
/// <summary>
/// Fast simple moving average period.
/// </summary>
public int FastPeriod
{
get => _fastPeriod.Value;
set => _fastPeriod.Value = value;
}
/// <summary>
/// Slow simple moving average period.
/// </summary>
public int SlowPeriod
{
get => _slowPeriod.Value;
set => _slowPeriod.Value = value;
}
/// <summary>
/// Stochastic oscillator %K length.
/// </summary>
public int StochasticKPeriod
{
get => _stochasticKPeriod.Value;
set => _stochasticKPeriod.Value = value;
}
/// <summary>
/// Stochastic oscillator %D smoothing length.
/// </summary>
public int StochasticDPeriod
{
get => _stochasticDPeriod.Value;
set => _stochasticDPeriod.Value = value;
}
/// <summary>
/// Additional smoothing applied to the %K line.
/// </summary>
public int StochasticSlowing
{
get => _stochasticSlowing.Value;
set => _stochasticSlowing.Value = value;
}
/// <summary>
/// Take-profit distance in pips relative to the average entry price.
/// </summary>
public decimal TakeProfitPips
{
get => _takeProfitPips.Value;
set => _takeProfitPips.Value = value;
}
/// <summary>
/// Stop-loss distance in pips relative to the average entry price.
/// </summary>
public decimal StopLossPips
{
get => _stopLossPips.Value;
set => _stopLossPips.Value = value;
}
/// <summary>
/// Initializes a new instance of <see cref="SmartTrendFollowerStrategy"/>.
/// </summary>
public SmartTrendFollowerStrategy()
{
_signalMode = Param(nameof(SignalMode), SignalModes.CrossMa)
.SetDisplay("Signal Mode", "Trading logic selection", "Signals");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(30).TimeFrame())
.SetDisplay("Candle Type", "Primary timeframe", "General");
_initialVolume = Param(nameof(InitialVolume), 1m)
.SetGreaterThanZero()
.SetDisplay("Initial Volume", "Starting order volume in lots", "Money Management");
_multiplier = Param(nameof(Multiplier), 2m)
.SetNotNegative()
.SetDisplay("Volume Multiplier", "Martingale multiplier applied to additional entries", "Money Management");
_layerDistancePips = Param(nameof(LayerDistancePips), 200m)
.SetNotNegative()
.SetDisplay("Layer Distance", "Pip distance before adding another order", "Money Management");
_fastPeriod = Param(nameof(FastPeriod), 14)
.SetGreaterThanZero()
.SetDisplay("Fast MA", "Fast moving average period", "Indicators")
;
_slowPeriod = Param(nameof(SlowPeriod), 28)
.SetGreaterThanZero()
.SetDisplay("Slow MA", "Slow moving average period", "Indicators")
;
_stochasticKPeriod = Param(nameof(StochasticKPeriod), 10)
.SetGreaterThanZero()
.SetDisplay("Stochastic %K", "%K lookback length", "Indicators");
_stochasticDPeriod = Param(nameof(StochasticDPeriod), 3)
.SetGreaterThanZero()
.SetDisplay("Stochastic %D", "%D smoothing length", "Indicators");
_stochasticSlowing = Param(nameof(StochasticSlowing), 3)
.SetGreaterThanZero()
.SetDisplay("Stochastic Slowing", "Extra smoothing for %K", "Indicators");
_takeProfitPips = Param(nameof(TakeProfitPips), 500m)
.SetNotNegative()
.SetDisplay("Take Profit", "Target distance in pips", "Risk Management");
_stopLossPips = Param(nameof(StopLossPips), 0m)
.SetNotNegative()
.SetDisplay("Stop Loss", "Protective distance in pips", "Risk Management");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_fastSma = null;
_slowSma = null;
_stochastic = null;
_longEntries.Clear();
_shortEntries.Clear();
_prevFast = null;
_prevSlow = null;
_pipSize = 0m;
_longExitRequested = false;
_shortExitRequested = false;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_fastSma = new SimpleMovingAverage { Length = Math.Max(1, FastPeriod) };
_slowSma = new SimpleMovingAverage { Length = Math.Max(1, SlowPeriod) };
_stochastic = new StochasticOscillator();
_stochastic.K.Length = Math.Max(1, StochasticKPeriod);
_stochastic.D.Length = Math.Max(1, StochasticDPeriod);
var subscription = SubscribeCandles(CandleType);
subscription
.BindEx(_fastSma, _slowSma, _stochastic, ProcessCandle)
.Start();
_pipSize = CalculatePipSize();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, _fastSma);
DrawIndicator(area, _slowSma);
DrawIndicator(area, _stochastic);
DrawOwnTrades(area);
}
// protection managed manually via ManageExits
}
/// <inheritdoc />
protected override void OnOwnTradeReceived(MyTrade trade)
{
base.OnOwnTradeReceived(trade);
var price = trade.Trade.Price;
var volume = trade.Trade.Volume;
if (trade.Order.Side == Sides.Buy)
{
ReduceEntries(_shortEntries, ref volume);
if (volume > 0m)
{
_longEntries.Add(new PositionEntry(price, volume));
}
}
else if (trade.Order.Side == Sides.Sell)
{
ReduceEntries(_longEntries, ref volume);
if (volume > 0m)
{
_shortEntries.Add(new PositionEntry(price, volume));
}
}
if (GetTotalVolume(_longEntries) <= 0m)
{
_longEntries.Clear();
_longExitRequested = false;
}
if (GetTotalVolume(_shortEntries) <= 0m)
{
_shortEntries.Clear();
_shortExitRequested = false;
}
}
private void ProcessCandle(ICandleMessage candle, IIndicatorValue fastValue, IIndicatorValue slowValue, IIndicatorValue stochasticValue)
{
if (candle.State != CandleStates.Finished)
return;
var fast = fastValue.ToDecimal();
var slow = slowValue.ToDecimal();
ManageExits(candle);
var signal = SignalDirections.None;
if (SignalMode == SignalModes.CrossMa)
{
if (_prevFast.HasValue && _prevSlow.HasValue)
{
var crossBuy = fast < slow && _prevSlow.Value < _prevFast.Value;
var crossSell = fast > slow && _prevSlow.Value > _prevFast.Value;
if (crossBuy)
signal = SignalDirections.Buy;
else if (crossSell)
signal = SignalDirections.Sell;
}
}
else if (_stochastic?.IsFormed == true)
{
var kValue = stochasticValue.ToDecimal();
var bullish = candle.ClosePrice > candle.OpenPrice;
var bearish = candle.ClosePrice < candle.OpenPrice;
if (fast > slow && bullish && kValue <= 30m)
signal = SignalDirections.Buy;
else if (fast < slow && bearish && kValue >= 70m)
signal = SignalDirections.Sell;
}
if (signal != SignalDirections.None)
{
ProcessSignal(signal, candle.ClosePrice);
}
_prevFast = fast;
_prevSlow = slow;
}
private void ProcessSignal(SignalDirections signal, decimal referencePrice)
{
switch (signal)
{
case SignalDirections.Buy:
{
var shortVolume = GetTotalVolume(_shortEntries);
if (shortVolume > 0m)
{
if (!_shortExitRequested)
{
_shortExitRequested = true;
BuyMarket(shortVolume);
}
return;
}
var longCount = _longEntries.Count;
var requested = CalculateRequestedVolume(longCount);
var volume = PrepareNextVolume(requested);
if (volume <= 0m)
return;
if (longCount == 0)
{
BuyMarket(volume);
return;
}
var lowest = GetExtremePrice(_longEntries, true);
var threshold = lowest - LayerDistancePips * (_pipSize > 0m ? _pipSize : 1m);
if (referencePrice <= threshold)
{
BuyMarket(volume);
}
break;
}
case SignalDirections.Sell:
{
var longVolume = GetTotalVolume(_longEntries);
if (longVolume > 0m)
{
if (!_longExitRequested)
{
_longExitRequested = true;
SellMarket(longVolume);
}
return;
}
var shortCount = _shortEntries.Count;
var requested = CalculateRequestedVolume(shortCount);
var volume = PrepareNextVolume(requested);
if (volume <= 0m)
return;
if (shortCount == 0)
{
SellMarket(volume);
return;
}
var highest = GetExtremePrice(_shortEntries, false);
var threshold = highest + LayerDistancePips * (_pipSize > 0m ? _pipSize : 1m);
if (referencePrice >= threshold)
{
SellMarket(volume);
}
break;
}
}
}
private void ManageExits(ICandleMessage candle)
{
var longVolume = GetTotalVolume(_longEntries);
if (longVolume > 0m && !_longExitRequested)
{
var average = GetAveragePrice(_longEntries);
var takeProfit = TakeProfitPips > 0m ? average + TakeProfitPips * (_pipSize > 0m ? _pipSize : 1m) : (decimal?)null;
var stopLoss = StopLossPips > 0m ? average - StopLossPips * (_pipSize > 0m ? _pipSize : 1m) : (decimal?)null;
if (takeProfit.HasValue && candle.HighPrice >= takeProfit.Value)
{
_longExitRequested = true;
SellMarket(longVolume);
return;
}
if (stopLoss.HasValue && candle.LowPrice <= stopLoss.Value)
{
_longExitRequested = true;
SellMarket(longVolume);
return;
}
}
var shortVolume = GetTotalVolume(_shortEntries);
if (shortVolume > 0m && !_shortExitRequested)
{
var average = GetAveragePrice(_shortEntries);
var takeProfit = TakeProfitPips > 0m ? average - TakeProfitPips * (_pipSize > 0m ? _pipSize : 1m) : (decimal?)null;
var stopLoss = StopLossPips > 0m ? average + StopLossPips * (_pipSize > 0m ? _pipSize : 1m) : (decimal?)null;
if (takeProfit.HasValue && candle.LowPrice <= takeProfit.Value)
{
_shortExitRequested = true;
BuyMarket(shortVolume);
return;
}
if (stopLoss.HasValue && candle.HighPrice >= stopLoss.Value)
{
_shortExitRequested = true;
BuyMarket(shortVolume);
}
}
}
private decimal CalculateRequestedVolume(int existingCount)
{
if (InitialVolume <= 0m)
return 0m;
var result = InitialVolume;
if (existingCount > 0 && Multiplier > 0m)
{
result *= (decimal)Math.Pow((double)Math.Max(Multiplier, 1m), existingCount);
}
return result;
}
private decimal PrepareNextVolume(decimal requested)
{
if (requested <= 0m)
return 0m;
var security = Security;
if (security == null)
return requested;
var step = security.VolumeStep ?? 0m;
if (step > 0m)
{
requested = step * Math.Round(requested / step, MidpointRounding.AwayFromZero);
}
var min = security.MinVolume ?? 0m;
if (min > 0m && requested < min)
return 0m;
var max = security.MaxVolume ?? decimal.MaxValue;
if (requested > max)
{
requested = max;
}
return requested;
}
private void ReduceEntries(List<PositionEntry> entries, ref decimal volume)
{
var index = 0;
while (volume > 0m && index < entries.Count)
{
var entry = entries[index];
if (volume >= entry.Volume)
{
volume -= entry.Volume;
entries.RemoveAt(index);
}
else
{
entry.Volume -= volume;
volume = 0m;
entries[index] = entry;
}
}
}
private static decimal GetTotalVolume(List<PositionEntry> entries)
{
var total = 0m;
for (var i = 0; i < entries.Count; i++)
total += entries[i].Volume;
return total;
}
private static decimal GetAveragePrice(List<PositionEntry> entries)
{
var totalVolume = GetTotalVolume(entries);
if (totalVolume <= 0m)
return 0m;
var weighted = 0m;
for (var i = 0; i < entries.Count; i++)
weighted += entries[i].Price * entries[i].Volume;
return weighted / totalVolume;
}
private static decimal GetExtremePrice(List<PositionEntry> entries, bool forLong)
{
if (entries.Count == 0)
return 0m;
var extreme = entries[0].Price;
for (var i = 1; i < entries.Count; i++)
{
var price = entries[i].Price;
if (forLong)
{
if (price < extreme)
extreme = price;
}
else if (price > extreme)
{
extreme = price;
}
}
return extreme;
}
private decimal CalculatePipSize()
{
var security = Security;
if (security == null)
return 0m;
var step = security.PriceStep ?? 0m;
if (step <= 0m)
return 0m;
var decimals = security.Decimals;
if (decimals == 3 || decimals == 5)
return step * 10m;
return step;
}
private enum SignalDirections
{
None,
Buy,
Sell
}
/// <summary>
/// Signal selector for the strategy.
/// </summary>
public enum SignalModes
{
/// <summary>
/// Use moving average crossovers in a contrarian fashion.
/// </summary>
CrossMa,
/// <summary>
/// Follow trend direction using moving averages with stochastic confirmation.
/// </summary>
Trend
}
private sealed class PositionEntry
{
public PositionEntry(decimal price, decimal volume)
{
Price = price;
Volume = volume;
}
public decimal Price { get; }
public decimal Volume { get; set; }
}
}
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.Indicators import SimpleMovingAverage, StochasticOscillator
from StockSharp.Algo.Strategies import Strategy
class smart_trend_follower_strategy(Strategy):
# Signal mode constants
CROSS_MA = 0
TREND = 1
def __init__(self):
super(smart_trend_follower_strategy, self).__init__()
self._signal_mode = self.Param("SignalMode", self.CROSS_MA) \
.SetDisplay("Signal Mode", "Trading logic selection", "Signals")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(30))) \
.SetDisplay("Candle Type", "Primary timeframe", "General")
self._initial_volume = self.Param("InitialVolume", 1.0) \
.SetGreaterThanZero() \
.SetDisplay("Initial Volume", "Starting order volume in lots", "Money Management")
self._multiplier = self.Param("Multiplier", 2.0) \
.SetNotNegative() \
.SetDisplay("Volume Multiplier", "Martingale multiplier applied to additional entries", "Money Management")
self._layer_distance_pips = self.Param("LayerDistancePips", 200.0) \
.SetNotNegative() \
.SetDisplay("Layer Distance", "Pip distance before adding another order", "Money Management")
self._fast_period = self.Param("FastPeriod", 14) \
.SetGreaterThanZero() \
.SetDisplay("Fast MA", "Fast moving average period", "Indicators")
self._slow_period = self.Param("SlowPeriod", 28) \
.SetGreaterThanZero() \
.SetDisplay("Slow MA", "Slow moving average period", "Indicators")
self._stochastic_k_period = self.Param("StochasticKPeriod", 10) \
.SetGreaterThanZero() \
.SetDisplay("Stochastic %K", "%K lookback length", "Indicators")
self._stochastic_d_period = self.Param("StochasticDPeriod", 3) \
.SetGreaterThanZero() \
.SetDisplay("Stochastic %D", "%D smoothing length", "Indicators")
self._stochastic_slowing = self.Param("StochasticSlowing", 3) \
.SetGreaterThanZero() \
.SetDisplay("Stochastic Slowing", "Extra smoothing for %K", "Indicators")
self._take_profit_pips = self.Param("TakeProfitPips", 500.0) \
.SetNotNegative() \
.SetDisplay("Take Profit", "Target distance in pips", "Risk Management")
self._stop_loss_pips = self.Param("StopLossPips", 0.0) \
.SetNotNegative() \
.SetDisplay("Stop Loss", "Protective distance in pips", "Risk Management")
self._fast_sma = None
self._slow_sma = None
self._stochastic = None
self._long_entries = []
self._short_entries = []
self._prev_fast = None
self._prev_slow = None
self._pip_size = 0.0
self._long_exit_requested = False
self._short_exit_requested = False
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
@property
def SignalMode(self):
return self._signal_mode.Value
@SignalMode.setter
def SignalMode(self, value):
self._signal_mode.Value = value
@property
def InitialVolume(self):
return self._initial_volume.Value
@InitialVolume.setter
def InitialVolume(self, value):
self._initial_volume.Value = value
@property
def Multiplier(self):
return self._multiplier.Value
@Multiplier.setter
def Multiplier(self, value):
self._multiplier.Value = value
@property
def LayerDistancePips(self):
return self._layer_distance_pips.Value
@LayerDistancePips.setter
def LayerDistancePips(self, value):
self._layer_distance_pips.Value = value
@property
def FastPeriod(self):
return self._fast_period.Value
@FastPeriod.setter
def FastPeriod(self, value):
self._fast_period.Value = value
@property
def SlowPeriod(self):
return self._slow_period.Value
@SlowPeriod.setter
def SlowPeriod(self, value):
self._slow_period.Value = value
@property
def StochasticKPeriod(self):
return self._stochastic_k_period.Value
@StochasticKPeriod.setter
def StochasticKPeriod(self, value):
self._stochastic_k_period.Value = value
@property
def StochasticDPeriod(self):
return self._stochastic_d_period.Value
@StochasticDPeriod.setter
def StochasticDPeriod(self, value):
self._stochastic_d_period.Value = value
@property
def StochasticSlowing(self):
return self._stochastic_slowing.Value
@StochasticSlowing.setter
def StochasticSlowing(self, value):
self._stochastic_slowing.Value = value
@property
def TakeProfitPips(self):
return self._take_profit_pips.Value
@TakeProfitPips.setter
def TakeProfitPips(self, value):
self._take_profit_pips.Value = value
@property
def StopLossPips(self):
return self._stop_loss_pips.Value
@StopLossPips.setter
def StopLossPips(self, value):
self._stop_loss_pips.Value = value
def OnReseted(self):
super(smart_trend_follower_strategy, self).OnReseted()
self._fast_sma = None
self._slow_sma = None
self._stochastic = None
self._long_entries = []
self._short_entries = []
self._prev_fast = None
self._prev_slow = None
self._pip_size = 0.0
self._long_exit_requested = False
self._short_exit_requested = False
def OnStarted2(self, time):
super(smart_trend_follower_strategy, self).OnStarted2(time)
self._fast_sma = SimpleMovingAverage()
self._fast_sma.Length = max(1, self.FastPeriod)
self._slow_sma = SimpleMovingAverage()
self._slow_sma.Length = max(1, self.SlowPeriod)
self._stochastic = StochasticOscillator()
self._stochastic.K.Length = max(1, self.StochasticKPeriod)
self._stochastic.D.Length = max(1, self.StochasticDPeriod)
subscription = self.SubscribeCandles(self.CandleType)
subscription \
.Bind(self._fast_sma, self._slow_sma, self._process_candle) \
.Start()
self._pip_size = self._calculate_pip_size()
def OnOwnTradeReceived(self, trade):
super(smart_trend_follower_strategy, self).OnOwnTradeReceived(trade)
price = float(trade.Trade.Price)
volume = float(trade.Trade.Volume)
if trade.Order.Side == Sides.Buy:
volume = self._reduce_entries(self._short_entries, volume)
if volume > 0:
self._long_entries.append([price, volume])
elif trade.Order.Side == Sides.Sell:
volume = self._reduce_entries(self._long_entries, volume)
if volume > 0:
self._short_entries.append([price, volume])
if self._get_total_volume(self._long_entries) <= 0:
self._long_entries.clear()
self._long_exit_requested = False
if self._get_total_volume(self._short_entries) <= 0:
self._short_entries.clear()
self._short_exit_requested = False
def _process_candle(self, candle, fast_value, slow_value):
if candle.State != CandleStates.Finished:
return
fast = float(fast_value)
slow = float(slow_value)
self._manage_exits(candle)
# Signal detection
signal = 0 # 0=None, 1=Buy, 2=Sell
if self.SignalMode == self.CROSS_MA:
if self._prev_fast is not None and self._prev_slow is not None:
cross_buy = fast < slow and self._prev_slow < self._prev_fast
cross_sell = fast > slow and self._prev_slow > self._prev_fast
if cross_buy:
signal = 1
elif cross_sell:
signal = 2
else:
bullish = float(candle.ClosePrice) > float(candle.OpenPrice)
bearish = float(candle.ClosePrice) < float(candle.OpenPrice)
if fast > slow and bullish:
signal = 1
elif fast < slow and bearish:
signal = 2
if signal != 0:
self._process_signal(signal, float(candle.ClosePrice))
self._prev_fast = fast
self._prev_slow = slow
def _process_signal(self, signal, reference_price):
pip = self._pip_size if self._pip_size > 0 else 1.0
if signal == 1: # Buy
short_volume = self._get_total_volume(self._short_entries)
if short_volume > 0:
if not self._short_exit_requested:
self._short_exit_requested = True
self.BuyMarket(float(short_volume))
return
long_count = len(self._long_entries)
requested = self._calculate_requested_volume(long_count)
volume = self._prepare_next_volume(requested)
if volume <= 0:
return
if long_count == 0:
self.BuyMarket(float(volume))
return
lowest = self._get_extreme_price(self._long_entries, True)
threshold = lowest - float(self.LayerDistancePips) * pip
if reference_price <= threshold:
self.BuyMarket(float(volume))
elif signal == 2: # Sell
long_volume = self._get_total_volume(self._long_entries)
if long_volume > 0:
if not self._long_exit_requested:
self._long_exit_requested = True
self.SellMarket(float(long_volume))
return
short_count = len(self._short_entries)
requested = self._calculate_requested_volume(short_count)
volume = self._prepare_next_volume(requested)
if volume <= 0:
return
if short_count == 0:
self.SellMarket(float(volume))
return
highest = self._get_extreme_price(self._short_entries, False)
threshold = highest + float(self.LayerDistancePips) * pip
if reference_price >= threshold:
self.SellMarket(float(volume))
def _manage_exits(self, candle):
pip = self._pip_size if self._pip_size > 0 else 1.0
long_volume = self._get_total_volume(self._long_entries)
if long_volume > 0 and not self._long_exit_requested:
average = self._get_average_price(self._long_entries)
take_profit = average + float(self.TakeProfitPips) * pip if float(self.TakeProfitPips) > 0 else None
stop_loss = average - float(self.StopLossPips) * pip if float(self.StopLossPips) > 0 else None
if take_profit is not None and float(candle.HighPrice) >= take_profit:
self._long_exit_requested = True
self.SellMarket(float(long_volume))
return
if stop_loss is not None and float(candle.LowPrice) <= stop_loss:
self._long_exit_requested = True
self.SellMarket(float(long_volume))
return
short_volume = self._get_total_volume(self._short_entries)
if short_volume > 0 and not self._short_exit_requested:
average = self._get_average_price(self._short_entries)
take_profit = average - float(self.TakeProfitPips) * pip if float(self.TakeProfitPips) > 0 else None
stop_loss = average + float(self.StopLossPips) * pip if float(self.StopLossPips) > 0 else None
if take_profit is not None and float(candle.LowPrice) <= take_profit:
self._short_exit_requested = True
self.BuyMarket(float(short_volume))
return
if stop_loss is not None and float(candle.HighPrice) >= stop_loss:
self._short_exit_requested = True
self.BuyMarket(float(short_volume))
def _calculate_requested_volume(self, existing_count):
if self.InitialVolume <= 0:
return 0.0
result = float(self.InitialVolume)
if existing_count > 0 and self.Multiplier > 0:
result *= float(self.Multiplier) ** existing_count if float(self.Multiplier) >= 1 else 1.0
return result
def _prepare_next_volume(self, requested):
if requested <= 0:
return 0.0
return requested
@staticmethod
def _reduce_entries(entries, volume):
idx = 0
while volume > 0 and idx < len(entries):
entry = entries[idx]
if volume >= entry[1]:
volume -= entry[1]
entries.pop(idx)
else:
entry[1] -= volume
volume = 0
return volume
@staticmethod
def _get_total_volume(entries):
total = 0.0
for e in entries:
total += e[1]
return total
@staticmethod
def _get_average_price(entries):
total_vol = 0.0
weighted = 0.0
for e in entries:
weighted += e[0] * e[1]
total_vol += e[1]
if total_vol <= 0:
return 0.0
return weighted / total_vol
@staticmethod
def _get_extreme_price(entries, for_long):
if len(entries) == 0:
return 0.0
extreme = entries[0][0]
for i in range(1, len(entries)):
price = entries[i][0]
if for_long:
if price < extreme:
extreme = price
else:
if price > extreme:
extreme = price
return extreme
def _calculate_pip_size(self):
security = self.Security
if security is None:
return 0.0
step = security.PriceStep
if step is None or float(step) <= 0:
return 0.0
decimals = security.Decimals
if decimals == 3 or decimals == 5:
return float(step) * 10.0
return float(step)
def CreateClone(self):
return smart_trend_follower_strategy()