EA Trix 策略
概述
EA Trix 策略复刻自 MetaTrader 5 的同名专家顾问,它将 TRIX ARROWS 指标与基础风控模块结合使用。当 TRIX 三重指数 均线与其信号线发生交叉时触发入场信号,并根据配置选择在信号 K 线收盘价立即执行或者在下一根 K 线开盘执行,以 符合原始脚本中的“收盘交易”选项。
交易逻辑
- 计算两条三重指数移动平均:
- TRIX 通过连续三次对收盘价应用长度为 TRIX EMA 的 EMA,并对第三次平滑的数值计算单根 K 线的变化率。
- 信号线采用相同的流程,但使用 Signal EMA 设定的长度。
- 通过交叉判断方向:
- 信号线从下向上穿越 TRIX 时准备开多单;
- 信号线从上向下穿越 TRIX 时准备开空单。
- 根据 Trade On Close 参数执行:
- 为
false时在当前信号 K 线收盘价直接下单; - 为
true时将信号挂起,等下一根 K 线开盘价执行,以模拟原始 EA 的延迟交易模式。
- 为
- 每次入场前都会平掉反向持仓,因此策略在任意时刻只持有一个方向的净头寸。
仓位管理
- Stop Loss:可选的固定止损距离,设置为
0表示不启用。 - Take Profit:可选的固定止盈距离,设置为
0表示不启用。 - Break Even:当价格向有利方向运行达到该距离后,止损价移动到开仓价实现保本。
- Trailing Stop:在达到追踪距离后启动移动止损,并按照 Trailing Step 指定的最小步长上调/下调止损价。
- 策略在每根完成的 K 线上使用最高价/最低价检查保护条件,一旦命中便通过市价单离场。
参数说明
| 参数 | 说明 |
|---|---|
CandleType |
策略订阅的 K 线类型(时间框架)。 |
Volume |
新仓位的下单数量,反向仓位会自动加入以完成反手。 |
EmaPeriod |
计算 TRIX 曲线所用 EMA 的周期。 |
SignalPeriod |
计算信号曲线所用 EMA 的周期。 |
TradeOnCloseBar |
true 表示在下一根 K 线开盘执行;false 表示在信号 K 线收盘执行。 |
StopLoss |
距离开仓价的止损幅度,0 关闭。 |
TakeProfit |
距离开仓价的止盈幅度,0 关闭。 |
TrailingStop |
启动移动止损所需的距离,0 关闭。 |
TrailingStep |
移动止损每次调整的最小步长。 |
BreakEven |
触发保本移动所需的距离,0 关闭。 |
使用建议
- 策略仅订阅一条蜡烛数据流,并且只处理已经收盘的 K 线,符合 StockSharp 高层 API 的使用规范。
- 止损、止盈及移动止损距离以价格单位表示,请根据交易品种的最小价位变动进行调整。
- 在回测或仿真中采用市价委托,默认以信号 K 线收盘价(或下一根 K 线开盘价)作为成交价。
转换说明
- 原版 MT5 专家顾问依赖外部指标 TRIX ARROWS(编号 19056)。本转换使用 StockSharp 的
ExponentialMovingAverage指标与单根变化率计算复现同样的结果,无需访问自定义缓冲区。 - MT5 中的服务器端止损/止盈在此实现中改为根据 K 线极值判断后发送市价单完成离场。
- 报警、声音提醒以及经纪商相关的辅助参数未迁移,因为它们不影响核心的交易逻辑。
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>
/// TRIX cross strategy based on the "TRIX ARROWS" expert advisor.
/// Opens a long position when the signal line crosses above TRIX and a short position on the opposite crossover.
/// Includes optional stop loss, take profit, break-even and trailing stop logic.
/// </summary>
public class EaTrixStrategy : Strategy
{
private enum SignalDirections
{
Buy,
Sell,
}
private readonly StrategyParam<decimal> _stopLoss;
private readonly StrategyParam<decimal> _takeProfit;
private readonly StrategyParam<decimal> _trailingStop;
private readonly StrategyParam<decimal> _trailingStep;
private readonly StrategyParam<decimal> _breakEven;
private readonly StrategyParam<bool> _tradeOnCloseBar;
private readonly StrategyParam<int> _emaPeriod;
private readonly StrategyParam<int> _signalPeriod;
private readonly StrategyParam<DataType> _candleType;
private ExponentialMovingAverage _trixEma1 = null!;
private ExponentialMovingAverage _trixEma2 = null!;
private ExponentialMovingAverage _trixEma3 = null!;
private ExponentialMovingAverage _signalEma1 = null!;
private ExponentialMovingAverage _signalEma2 = null!;
private ExponentialMovingAverage _signalEma3 = null!;
private decimal? _prevThirdTrix;
private decimal? _prevThirdSignal;
private decimal? _prevTrix;
private decimal? _prevSignal;
private SignalDirections? _pendingSignal;
private decimal? _entryPrice;
private decimal? _stopPrice;
private decimal? _takePrice;
/// <summary>
/// Stop loss distance in price units. Set to zero to disable.
/// </summary>
public decimal StopLoss
{
get => _stopLoss.Value;
set => _stopLoss.Value = value;
}
/// <summary>
/// Take profit distance in price units. Set to zero to disable.
/// </summary>
public decimal TakeProfit
{
get => _takeProfit.Value;
set => _takeProfit.Value = value;
}
/// <summary>
/// Trailing stop distance in price units. Set to zero to disable.
/// </summary>
public decimal TrailingStop
{
get => _trailingStop.Value;
set => _trailingStop.Value = value;
}
/// <summary>
/// Minimal step for trailing stop updates.
/// </summary>
public decimal TrailingStep
{
get => _trailingStep.Value;
set => _trailingStep.Value = value;
}
/// <summary>
/// Break-even trigger distance. The stop is moved to the entry price when the distance is reached.
/// </summary>
public decimal BreakEven
{
get => _breakEven.Value;
set => _breakEven.Value = value;
}
/// <summary>
/// Trade using signals confirmed on the previous closed bar.
/// When disabled the strategy reacts immediately on the bar that generated the crossover.
/// </summary>
public bool TradeOnCloseBar
{
get => _tradeOnCloseBar.Value;
set => _tradeOnCloseBar.Value = value;
}
/// <summary>
/// EMA length used to build the TRIX series.
/// </summary>
public int EmaPeriod
{
get => _emaPeriod.Value;
set => _emaPeriod.Value = value;
}
/// <summary>
/// EMA length used to build the signal series.
/// </summary>
public int SignalPeriod
{
get => _signalPeriod.Value;
set => _signalPeriod.Value = value;
}
/// <summary>
/// Candle type processed by the strategy.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initializes a new instance of <see cref="EaTrixStrategy"/>.
/// </summary>
public EaTrixStrategy()
{
_stopLoss = Param(nameof(StopLoss), 50m)
.SetNotNegative()
.SetDisplay("Stop Loss", "Stop loss distance", "Risk")
;
_takeProfit = Param(nameof(TakeProfit), 150m)
.SetNotNegative()
.SetDisplay("Take Profit", "Take profit distance", "Risk")
;
_trailingStop = Param(nameof(TrailingStop), 10m)
.SetNotNegative()
.SetDisplay("Trailing Stop", "Trailing stop distance", "Risk")
;
_trailingStep = Param(nameof(TrailingStep), 1m)
.SetNotNegative()
.SetDisplay("Trailing Step", "Minimal trailing step", "Risk")
;
_breakEven = Param(nameof(BreakEven), 2m)
.SetNotNegative()
.SetDisplay("Break Even", "Break-even trigger distance", "Risk")
;
_tradeOnCloseBar = Param(nameof(TradeOnCloseBar), true)
.SetDisplay("Trade On Close", "Confirm signals on closed bars", "General");
_emaPeriod = Param(nameof(EmaPeriod), 14)
.SetGreaterThanZero()
.SetDisplay("TRIX EMA", "TRIX EMA length", "Indicators")
;
_signalPeriod = Param(nameof(SignalPeriod), 8)
.SetGreaterThanZero()
.SetDisplay("Signal EMA", "Signal EMA length", "Indicators")
;
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
.SetDisplay("Candle Type", "Timeframe for calculations", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_prevThirdTrix = null;
_prevThirdSignal = null;
_prevTrix = null;
_prevSignal = null;
_pendingSignal = null;
ClearPositionState();
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
// no protection
_trixEma1 = new ExponentialMovingAverage { Length = EmaPeriod };
_trixEma2 = new ExponentialMovingAverage { Length = EmaPeriod };
_trixEma3 = new ExponentialMovingAverage { Length = EmaPeriod };
_signalEma1 = new ExponentialMovingAverage { Length = SignalPeriod };
_signalEma2 = new ExponentialMovingAverage { Length = SignalPeriod };
_signalEma3 = new ExponentialMovingAverage { Length = SignalPeriod };
var subscription = SubscribeCandles(CandleType);
subscription.Bind(ProcessCandle).Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
HandlePendingSignal(candle);
ManageActivePosition(candle);
if (!TryCalculateIndicators(candle, out var trix, out var signal))
return;
if (_prevTrix is null || _prevSignal is null)
{
_prevTrix = trix;
_prevSignal = signal;
return;
}
if (!_trixEma3.IsFormed || !_signalEma3.IsFormed)
{
_prevTrix = trix;
_prevSignal = signal;
return;
}
var crossUp = _prevSignal < _prevTrix && signal > trix;
var crossDown = _prevSignal > _prevTrix && signal < trix;
if (crossUp)
{
if (TradeOnCloseBar)
_pendingSignal = SignalDirections.Buy;
else
ExecuteSignal(SignalDirections.Buy, candle, candle.ClosePrice);
}
else if (crossDown)
{
if (TradeOnCloseBar)
_pendingSignal = SignalDirections.Sell;
else
ExecuteSignal(SignalDirections.Sell, candle, candle.ClosePrice);
}
_prevTrix = trix;
_prevSignal = signal;
}
private void HandlePendingSignal(ICandleMessage candle)
{
if (_pendingSignal is null)
return;
if (!_trixEma3.IsFormed || !_signalEma3.IsFormed)
return;
ExecuteSignal(_pendingSignal.Value, candle, candle.OpenPrice);
_pendingSignal = null;
}
private void ExecuteSignal(SignalDirections direction, ICandleMessage candle, decimal fillPrice)
{
if (Volume <= 0m)
return;
var volume = Volume;
switch (direction)
{
case SignalDirections.Buy:
if (Position < 0m)
volume += Math.Abs(Position);
if (volume > 0m)
BuyMarket();
_entryPrice = fillPrice;
_stopPrice = StopLoss > 0m ? fillPrice - StopLoss : null;
_takePrice = TakeProfit > 0m ? fillPrice + TakeProfit : null;
break;
case SignalDirections.Sell:
if (Position > 0m)
volume += Position;
if (volume > 0m)
SellMarket();
_entryPrice = fillPrice;
_stopPrice = StopLoss > 0m ? fillPrice + StopLoss : null;
_takePrice = TakeProfit > 0m ? fillPrice - TakeProfit : null;
break;
}
}
private void ManageActivePosition(ICandleMessage candle)
{
if (Position > 0m && _entryPrice is decimal longEntry)
{
if (BreakEven > 0m && candle.HighPrice - longEntry >= BreakEven && (_stopPrice is null || _stopPrice < longEntry))
_stopPrice = longEntry;
if (TrailingStop > 0m)
{
var move = candle.HighPrice - longEntry;
if (move >= TrailingStop)
{
var newStop = candle.HighPrice - TrailingStop;
if (_stopPrice is null || newStop - _stopPrice >= TrailingStep)
_stopPrice = newStop;
}
}
if (_takePrice is decimal tp && candle.HighPrice >= tp)
{
SellMarket();
ClearPositionState();
return;
}
if (_stopPrice is decimal sl && candle.LowPrice <= sl)
{
SellMarket();
ClearPositionState();
}
}
else if (Position < 0m && _entryPrice is decimal shortEntry)
{
if (BreakEven > 0m && shortEntry - candle.LowPrice >= BreakEven && (_stopPrice is null || _stopPrice > shortEntry))
_stopPrice = shortEntry;
if (TrailingStop > 0m)
{
var move = shortEntry - candle.LowPrice;
if (move >= TrailingStop)
{
var newStop = candle.LowPrice + TrailingStop;
if (_stopPrice is null || _stopPrice - newStop >= TrailingStep)
_stopPrice = newStop;
}
}
if (_takePrice is decimal tp && candle.LowPrice <= tp)
{
BuyMarket();
ClearPositionState();
return;
}
if (_stopPrice is decimal sl && candle.HighPrice >= sl)
{
BuyMarket();
ClearPositionState();
}
}
else if (Position == 0m)
{
ClearPositionState();
}
}
private bool TryCalculateIndicators(ICandleMessage candle, out decimal trix, out decimal signal)
{
trix = 0m;
signal = 0m;
var ema1 = _trixEma1.Process(new DecimalIndicatorValue(_trixEma1, candle.ClosePrice, candle.OpenTime) { IsFinal = true }).ToDecimal();
var ema2 = _trixEma2.Process(new DecimalIndicatorValue(_trixEma2, ema1, candle.OpenTime) { IsFinal = true }).ToDecimal();
var ema3 = _trixEma3.Process(new DecimalIndicatorValue(_trixEma3, ema2, candle.OpenTime) { IsFinal = true }).ToDecimal();
if (_prevThirdTrix is null)
{
_prevThirdTrix = ema3;
return false;
}
trix = _prevThirdTrix != 0m ? (ema3 - _prevThirdTrix.Value) / _prevThirdTrix.Value : 0m;
_prevThirdTrix = ema3;
var signal1 = _signalEma1.Process(new DecimalIndicatorValue(_signalEma1, candle.ClosePrice, candle.OpenTime) { IsFinal = true }).ToDecimal();
var signal2 = _signalEma2.Process(new DecimalIndicatorValue(_signalEma2, signal1, candle.OpenTime) { IsFinal = true }).ToDecimal();
var signalBase = _signalEma3.Process(new DecimalIndicatorValue(_signalEma3, signal2, candle.OpenTime) { IsFinal = true }).ToDecimal();
if (_prevThirdSignal is null)
{
_prevThirdSignal = signalBase;
return false;
}
signal = _prevThirdSignal != 0m ? (signalBase - _prevThirdSignal.Value) / _prevThirdSignal.Value : 0m;
_prevThirdSignal = signalBase;
return true;
}
private void ClearPositionState()
{
_entryPrice = null;
_stopPrice = null;
_takePrice = null;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Decimal
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Strategies import Strategy
from StockSharp.Algo.Indicators import ExponentialMovingAverage
from indicator_extensions import *
class ea_trix_strategy(Strategy):
"""TRIX cross strategy: signal line crosses above/below TRIX with SL/TP, break-even and trailing."""
# Signal directions
_BUY = 1
_SELL = -1
def __init__(self):
super(ea_trix_strategy, self).__init__()
self._stop_loss = self.Param("StopLoss", 50.0) \
.SetDisplay("Stop Loss", "Stop loss distance", "Risk")
self._take_profit = self.Param("TakeProfit", 150.0) \
.SetDisplay("Take Profit", "Take profit distance", "Risk")
self._trailing_stop = self.Param("TrailingStop", 10.0) \
.SetDisplay("Trailing Stop", "Trailing stop distance", "Risk")
self._trailing_step = self.Param("TrailingStep", 1.0) \
.SetDisplay("Trailing Step", "Minimal trailing step", "Risk")
self._break_even = self.Param("BreakEven", 2.0) \
.SetDisplay("Break Even", "Break-even trigger distance", "Risk")
self._trade_on_close_bar = self.Param("TradeOnCloseBar", True) \
.SetDisplay("Trade On Close", "Confirm signals on closed bars", "General")
self._ema_period = self.Param("EmaPeriod", 14) \
.SetGreaterThanZero() \
.SetDisplay("TRIX EMA", "TRIX EMA length", "Indicators")
self._signal_period = self.Param("SignalPeriod", 8) \
.SetGreaterThanZero() \
.SetDisplay("Signal EMA", "Signal EMA length", "Indicators")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4))) \
.SetDisplay("Candle Type", "Timeframe for calculations", "General")
self._prev_third_trix = None
self._prev_third_signal = None
self._prev_trix = None
self._prev_signal = None
self._pending_signal = None
self._entry_price = None
self._stop_price = None
self._take_price = None
@property
def StopLoss(self):
return float(self._stop_loss.Value)
@property
def TakeProfit(self):
return float(self._take_profit.Value)
@property
def TrailingStop(self):
return float(self._trailing_stop.Value)
@property
def TrailingStep(self):
return float(self._trailing_step.Value)
@property
def BreakEven(self):
return float(self._break_even.Value)
@property
def TradeOnCloseBar(self):
return self._trade_on_close_bar.Value
@property
def EmaPeriod(self):
return int(self._ema_period.Value)
@property
def SignalPeriod(self):
return int(self._signal_period.Value)
@property
def CandleType(self):
return self._candle_type.Value
def OnStarted2(self, time):
super(ea_trix_strategy, self).OnStarted2(time)
self._prev_third_trix = None
self._prev_third_signal = None
self._prev_trix = None
self._prev_signal = None
self._pending_signal = None
self._clear_position_state()
self._trix_ema1 = ExponentialMovingAverage()
self._trix_ema1.Length = self.EmaPeriod
self._trix_ema2 = ExponentialMovingAverage()
self._trix_ema2.Length = self.EmaPeriod
self._trix_ema3 = ExponentialMovingAverage()
self._trix_ema3.Length = self.EmaPeriod
self._signal_ema1 = ExponentialMovingAverage()
self._signal_ema1.Length = self.SignalPeriod
self._signal_ema2 = ExponentialMovingAverage()
self._signal_ema2.Length = self.SignalPeriod
self._signal_ema3 = ExponentialMovingAverage()
self._signal_ema3.Length = self.SignalPeriod
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self.process_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawOwnTrades(area)
def process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
self._handle_pending_signal(candle)
self._manage_active_position(candle)
result = self._try_calculate_indicators(candle)
if result is None:
return
trix, signal = result
if self._prev_trix is None or self._prev_signal is None:
self._prev_trix = trix
self._prev_signal = signal
return
if not self._trix_ema3.IsFormed or not self._signal_ema3.IsFormed:
self._prev_trix = trix
self._prev_signal = signal
return
cross_up = self._prev_signal < self._prev_trix and signal > trix
cross_down = self._prev_signal > self._prev_trix and signal < trix
if cross_up:
if self.TradeOnCloseBar:
self._pending_signal = self._BUY
else:
self._execute_signal(self._BUY, candle, float(candle.ClosePrice))
elif cross_down:
if self.TradeOnCloseBar:
self._pending_signal = self._SELL
else:
self._execute_signal(self._SELL, candle, float(candle.ClosePrice))
self._prev_trix = trix
self._prev_signal = signal
def _handle_pending_signal(self, candle):
if self._pending_signal is None:
return
if not self._trix_ema3.IsFormed or not self._signal_ema3.IsFormed:
return
self._execute_signal(self._pending_signal, candle, float(candle.OpenPrice))
self._pending_signal = None
def _execute_signal(self, direction, candle, fill_price):
if direction == self._BUY:
if self.Position < 0:
self.BuyMarket()
self.BuyMarket()
self._entry_price = fill_price
self._stop_price = fill_price - self.StopLoss if self.StopLoss > 0 else None
self._take_price = fill_price + self.TakeProfit if self.TakeProfit > 0 else None
elif direction == self._SELL:
if self.Position > 0:
self.SellMarket()
self.SellMarket()
self._entry_price = fill_price
self._stop_price = fill_price + self.StopLoss if self.StopLoss > 0 else None
self._take_price = fill_price - self.TakeProfit if self.TakeProfit > 0 else None
def _manage_active_position(self, candle):
h = float(candle.HighPrice)
lo = float(candle.LowPrice)
if self.Position > 0 and self._entry_price is not None:
long_entry = self._entry_price
# Break-even
if self.BreakEven > 0 and h - long_entry >= self.BreakEven:
if self._stop_price is None or self._stop_price < long_entry:
self._stop_price = long_entry
# Trailing stop
if self.TrailingStop > 0:
move = h - long_entry
if move >= self.TrailingStop:
new_stop = h - self.TrailingStop
if self._stop_price is None or new_stop - self._stop_price >= self.TrailingStep:
self._stop_price = new_stop
# Take profit
if self._take_price is not None and h >= self._take_price:
self.SellMarket()
self._clear_position_state()
return
# Stop loss
if self._stop_price is not None and lo <= self._stop_price:
self.SellMarket()
self._clear_position_state()
elif self.Position < 0 and self._entry_price is not None:
short_entry = self._entry_price
# Break-even
if self.BreakEven > 0 and short_entry - lo >= self.BreakEven:
if self._stop_price is None or self._stop_price > short_entry:
self._stop_price = short_entry
# Trailing stop
if self.TrailingStop > 0:
move = short_entry - lo
if move >= self.TrailingStop:
new_stop = lo + self.TrailingStop
if self._stop_price is None or self._stop_price - new_stop >= self.TrailingStep:
self._stop_price = new_stop
# Take profit
if self._take_price is not None and lo <= self._take_price:
self.BuyMarket()
self._clear_position_state()
return
# Stop loss
if self._stop_price is not None and h >= self._stop_price:
self.BuyMarket()
self._clear_position_state()
elif self.Position == 0:
self._clear_position_state()
def _make_div(self, ind, val, t):
return float(process_float(ind, Decimal(val), t, True).Value)
def _try_calculate_indicators(self, candle):
close = float(candle.ClosePrice)
t = candle.ServerTime
ema1_val = self._make_div(self._trix_ema1, close, t)
ema2_val = self._make_div(self._trix_ema2, ema1_val, t)
ema3_val = self._make_div(self._trix_ema3, ema2_val, t)
if self._prev_third_trix is None:
self._prev_third_trix = ema3_val
return None
trix = (ema3_val - self._prev_third_trix) / self._prev_third_trix if self._prev_third_trix != 0 else 0.0
self._prev_third_trix = ema3_val
sig1_val = self._make_div(self._signal_ema1, close, t)
sig2_val = self._make_div(self._signal_ema2, sig1_val, t)
sig_base = self._make_div(self._signal_ema3, sig2_val, t)
if self._prev_third_signal is None:
self._prev_third_signal = sig_base
return None
signal = (sig_base - self._prev_third_signal) / self._prev_third_signal if self._prev_third_signal != 0 else 0.0
self._prev_third_signal = sig_base
return (trix, signal)
def _clear_position_state(self):
self._entry_price = None
self._stop_price = None
self._take_price = None
def OnReseted(self):
super(ea_trix_strategy, self).OnReseted()
self._prev_third_trix = None
self._prev_third_signal = None
self._prev_trix = None
self._prev_signal = None
self._pending_signal = None
self._clear_position_state()
def CreateClone(self):
return ea_trix_strategy()