賢いトレンドフォロー戦略
概要
スマート トレンド フォロワー戦略は、MetaTrader 5 エキスパート アドバイザー スマート トレンド フォロワーの StockSharp 移植です。の オリジナルのシステムは、逆張り移動平均クロスオーバーと確率論を使用したトレンドフォローセットアップを交互に実行します。 確認。マーチンゲールのような出来高乗数でポジションにスケールし、共有のテイクプロフィット/ストップロスを維持します。 方向性のあるバスケットごとに。 StockSharp バージョンは、高レベルの API (candle サブスクリプション、指標バインディング、成行注文など)。
信号ロジック
2 つの独立した信号エンジンが利用可能で、SignalMode パラメータで切り替えることができます。
- CrossMa – オリジナルの逆張りクロスオーバーを再現します。高速の SMA が低速の SMA を下方で通過するとき (高速 < 低速) しかし、以前は高速 > 低速) 戦略はロング ポジションをオープンまたは平均します。高速の SMA が低速の *上を通過するとき SMA (速い > 遅いが、以前は速い < 遅い) ショートをオープンまたは平均化します。
- トレンド – 確率オシレーターからの確認を必要とする元のトレンド モードに従います。強気のシグナル 速い SMA が遅い SMA を上回っており、ローソク足が始値よりも高く終了し、確率的 %K 値が低い場合に表示されます。 弱気シグナルには、高速 < 低速、弱気ローソク体、確率的 %K が 70 以上である必要があります。
シグナルは完成したキャンドルのみで評価されます。反対のポジションがまだオープンしている間に新しいシグナルが到着すると、 この戦略では、まず反対側のバスケットを清算してから、新しいエントリーを処理して、バスケットの方向性を維持します。 現在の信号。
位置のスケーリング
この戦略は、MQL マーチンゲール ロジックを再現します。
- 最初の注文では
InitialVolumeロットが使用されます。 - 平均化注文を追加するたびに、前のボリュームに
Multiplierが乗算されます (値 ≤ 1 はボリュームの増加を無効にします)。 - アクティブ方向の新しい平均注文は、市場が
LayerDistancePipsピップ離れた後にのみ許可されます 現在のバスケットの最良のエントリー価格(最低のロング約定または最高のショート約定)から。 - ボリュームは、利用可能な場合、機器の
VolumeStep、VolumeMin、およびVolumeMaxの制限を使用して正規化されます。
リスク管理
各方向性バスケットについて、戦略は共有損益分岐点価格 (全約定の出来高加重平均) を追跡します。
TakeProfitPipsは、平均エントリー価格とバスケットのテイクプロフィットの間の距離を定義します。長いバスケットは次の時点で排出されます。 ローソク足の高値がそのレベルに触れると、ローソク足の安値がそのレベルに達するとバスケットがショートします。テイクプロフィットターゲットを無効にするには、0 に設定します。StopLossPipsは、保護出口の動作を反映します。ロングバスケットはローソク足がストップを下抜けたときに閉じます。 ローソク足の高値がその上を通過すると、バスケットが短くなります。保護停止を無効にするには、0 に設定します。
手仕舞い注文は、次の終了ローソク足がそのレベルに達したことが確認されたときに、成行注文を通じて実行されます。の
戦略は、約定中の重複したエグジット送信を避けるために、_longExitRequested フラグと _shortExitRequested フラグを維持します。
まだ保留中です。
パラメーター
| パラメータ | 種類 | デフォルト | 説明 |
|---|---|---|---|
SignalMode |
列挙型 (CrossMa、Trend) |
CrossMa |
信号エンジン (逆張りクロスオーバーまたは確率的フィルターを使用したトレンド) を選択します。 |
CandleType |
DataType |
30分の時間枠 | 計算と信号生成に使用される主なキャンドル シリーズ。 |
InitialVolume |
10進数 | 0.01 |
バスケットの最初のエントリのロット単位の基本注文サイズ。 |
Multiplier |
10進数 | 2 |
追加の平均化注文ごとに適用されるボリューム乗数。 |
LayerDistancePips |
10進数 | 200 |
同じ方向に別の注文を追加する前の最良のエントリーからの最小ピップ距離。 |
FastPeriod |
整数 | 14 |
高速単純移動平均の期間。 |
SlowPeriod |
整数 | 28 |
低速単純移動平均の期間 (FastPeriod より大きくなければなりません)。 |
StochasticKPeriod |
整数 | 10 |
確率オシレーター %K ラインのルックバック長。 |
StochasticDPeriod |
整数 | 3 |
確率的 %D ラインの平滑化長。 |
StochasticSlowing |
整数 | 3 |
%D 計算の前に、%K に追加の平滑化が適用されます。 |
TakeProfitPips |
10進数 | 500 |
バスケットのテイクプロフィットが配置される平均エントリーからの距離 (ピップ単位)。無効にするには 0 を設定します。 |
StopLossPips |
10進数 | 0 |
ピップ単位の保護停止距離。ハードストップを無効にするには、0 を設定します。 |
実装メモ
- ピップサイズは、商品
PriceStepとDecimalsから導出され、「ポイント」の MetaTrader の概念と一致します (例: 5 桁の為替相場の場合は 0.0001)。 - 位置追跡では、
PositionEntryオブジェクトの 2 つのリストを使用して、MetaTrader のチケットごとの会計を反映します。エントリは次のとおりです。 反対の取引がバスケットの一部を閉じる場合、FIFO スタイルが軽減されます。 - すべてのインジケーターの計算は、StockSharp の高レベルのバインディング API (
SubscribeCandles().BindEx(...)) に依存します。手動呼び出しはありませんGetValueまでが必須であり、インジケーターがStrategy.Indicatorsに挿入されることはありません。 - この戦略は開始時に
StartProtection()を呼び出し、StockSharp がグローバルなリスク制御モジュール (損益分岐点、 マージンチェックなど)。 - 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()