Improve MA & RSI Hedge 策略
本策略将 MetaTrader 的 “Improve” EA 迁移到 StockSharp 平台。它同时交易两个品种:在策略中选择的主品种以及用于对冲的品种。交易方向由主品种上的两条平滑移动平均线(SMMA)以及 RSI 指标共同决定。对冲腿与主腿保持同方向,从而构建协同的双品种头寸,目标是在相关资产同步运动时捕捉收益,同时降低单一品种的风险。
策略逻辑
- 在主品种上计算两条平滑移动平均线(SMMA),可分别设置快线与慢线周期。
- 计算同周期的 RSI,并监控超卖/超买水平。
- 当慢线位于快线上方且 RSI 低于或等于超卖阈值时,同时在两个品种上开多仓。
- 当慢线位于快线下方且 RSI 高于或等于超买阈值时,同时在两个品种上开空仓。
- 持仓一直保留,直到两个品种的合计浮动利润达到设定的货币目标,然后通过市价单同时平掉两条腿。
算法保存每个品种最新的收盘价,并使用入场价与当前价的差值估算总体利润。由于没有内置止损,如果价格未触及盈利目标,仓位可能会长期持有。
参数
| 参数 | 说明 |
|---|---|
| Volume | 主腿与对冲腿的下单数量。 |
| Profit Target | 两条腿共享的盈利目标(货币单位)。达到后立刻平仓。 |
| Hedge Security | 与主品种同时交易的对冲品种。 |
| Fast MA | 快速 SMMA 的周期(默认 8)。 |
| Slow MA | 慢速 SMMA 的周期(默认 21),必须大于快速周期。 |
| RSI Period | RSI 的计算长度(默认 21)。 |
| Oversold | RSI 的超卖阈值,满足条件并配合均线关系触发做多(默认 30)。 |
| Overbought | RSI 的超买阈值,满足条件并配合均线关系触发做空(默认 70)。 |
| Candle Type | 计算所用的蜡烛周期。默认 1 小时,可按需求调整。 |
使用指标
- 平滑移动平均线(SMMA):用于判定趋势方向(快/慢两条)。
- 相对强弱指标(RSI):用于识别超卖与超买区域。
入场与出场规则
- 做多
- 慢速 SMMA 高于快速 SMMA。
- RSI ≤ 超卖阈值。
- 在主品种和对冲品种上同时发送买入市价单。
- 做空
- 慢速 SMMA 低于快速 SMMA。
- RSI ≥ 超买阈值。
- 在两个品种上同时发送卖出市价单。
- 平仓
- 当
(主腿利润 + 对冲腿利润) ≥ Profit Target时,通过市价单同时平掉所有仓位。 - 策略不包含额外的止损或追踪止损,如需风险控制应另外实现。
- 当
使用建议
- 启动策略前必须指定主品种和对冲品种,否则策略会抛出异常。
- 合计利润基于蜡烛收盘价估算,实际成交可能受滑点和执行速度影响。
- 建议用于高度相关的品种组合(如相关货币对或同类期货),预期出现同步走势。
- 实盘时应结合额外的资金管理或组合风险控制,因为策略仅依赖虚拟盈利目标作为退出机制。
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;
using StockSharp.Algo;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Dual smoothed moving average and RSI hedge strategy converted from Improve.mq5.
/// </summary>
public class ImproveMaRsiHedgeStrategy : Strategy
{
private readonly StrategyParam<decimal> _profitTarget;
private readonly StrategyParam<Security> _hedgeSecurity;
private readonly StrategyParam<int> _fastPeriod;
private readonly StrategyParam<int> _slowPeriod;
private readonly StrategyParam<int> _rsiPeriod;
private readonly StrategyParam<decimal> _oversoldLevel;
private readonly StrategyParam<decimal> _overboughtLevel;
private readonly StrategyParam<DataType> _candleType;
private SmoothedMovingAverage _fastMa = null!;
private SmoothedMovingAverage _slowMa = null!;
private RelativeStrengthIndex _rsi = null!;
private decimal _baseLastClose;
private decimal _hedgeLastClose;
private decimal _baseEntryPrice;
private decimal _hedgeEntryPrice;
private bool _hasBaseClose;
private bool _hasHedgeClose;
private int _pairDirection;
/// <summary>
/// Profit target across both legs expressed in money.
/// </summary>
public decimal ProfitTarget
{
get => _profitTarget.Value;
set => _profitTarget.Value = value;
}
/// <summary>
/// Second instrument traded alongside the primary security.
/// </summary>
public Security HedgeSecurity
{
get => _hedgeSecurity.Value;
set => _hedgeSecurity.Value = value;
}
/// <summary>
/// Smoothed moving average period for the fast line.
/// </summary>
public int FastMaPeriod
{
get => _fastPeriod.Value;
set => _fastPeriod.Value = value;
}
/// <summary>
/// Smoothed moving average period for the slow line.
/// </summary>
public int SlowMaPeriod
{
get => _slowPeriod.Value;
set => _slowPeriod.Value = value;
}
/// <summary>
/// RSI calculation length.
/// </summary>
public int RsiPeriod
{
get => _rsiPeriod.Value;
set => _rsiPeriod.Value = value;
}
/// <summary>
/// RSI oversold threshold.
/// </summary>
public decimal OversoldLevel
{
get => _oversoldLevel.Value;
set => _oversoldLevel.Value = value;
}
/// <summary>
/// RSI overbought threshold.
/// </summary>
public decimal OverboughtLevel
{
get => _overboughtLevel.Value;
set => _overboughtLevel.Value = value;
}
/// <summary>
/// Type of candles used for calculations.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initializes a new instance of the <see cref="ImproveMaRsiHedgeStrategy"/> class.
/// </summary>
public ImproveMaRsiHedgeStrategy()
{
_profitTarget = Param(nameof(ProfitTarget), 50m)
.SetGreaterThanZero()
.SetDisplay("Profit Target", "Combined profit target across both legs", "Risk")
;
_hedgeSecurity = Param<Security>(nameof(HedgeSecurity))
.SetDisplay("Hedge Security", "Secondary instrument to trade", "General");
_fastPeriod = Param(nameof(FastMaPeriod), 8)
.SetGreaterThanZero()
.SetDisplay("Fast MA", "Fast smoothed MA period", "Indicators")
;
_slowPeriod = Param(nameof(SlowMaPeriod), 21)
.SetGreaterThanZero()
.SetDisplay("Slow MA", "Slow smoothed MA period", "Indicators")
;
_rsiPeriod = Param(nameof(RsiPeriod), 21)
.SetGreaterThanZero()
.SetDisplay("RSI Period", "Length of the RSI", "Indicators")
;
_oversoldLevel = Param(nameof(OversoldLevel), 30m)
.SetDisplay("Oversold", "RSI oversold threshold", "Indicators")
;
_overboughtLevel = Param(nameof(OverboughtLevel), 70m)
.SetDisplay("Overbought", "RSI overbought threshold", "Indicators")
;
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
.SetDisplay("Candle Type", "Time frame for calculations", "Data");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
if (Security != null)
yield return (Security, CandleType);
if (HedgeSecurity != null)
yield return (HedgeSecurity, CandleType);
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_fastMa = null!;
_slowMa = null!;
_rsi = null!;
_baseLastClose = 0m;
_hedgeLastClose = 0m;
_baseEntryPrice = 0m;
_hedgeEntryPrice = 0m;
_hasBaseClose = false;
_hasHedgeClose = false;
_pairDirection = 0;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
if (Security == null)
throw new InvalidOperationException("Primary security must be specified.");
if (HedgeSecurity == null)
throw new InvalidOperationException("Hedge security must be specified.");
if (FastMaPeriod >= SlowMaPeriod)
throw new InvalidOperationException("Fast MA period must be less than slow MA period.");
_fastMa = new SmoothedMovingAverage { Length = FastMaPeriod };
_slowMa = new SmoothedMovingAverage { Length = SlowMaPeriod };
_rsi = new RelativeStrengthIndex { Length = RsiPeriod };
var baseSubscription = SubscribeCandles(CandleType);
baseSubscription
.Bind(_fastMa, _slowMa, _rsi, ProcessBaseCandle)
.Start();
var hedgeSubscription = SubscribeCandles(CandleType, false, HedgeSecurity);
hedgeSubscription
.Bind(ProcessHedgeCandle)
.Start();
}
private void ProcessBaseCandle(ICandleMessage candle, decimal fastValue, decimal slowValue, decimal rsiValue)
{
if (candle.State != CandleStates.Finished)
return;
_baseLastClose = candle.ClosePrice;
_hasBaseClose = true;
CheckProfitTarget();
if (!IsFormedAndOnlineAndAllowTrading())
return;
if (!_fastMa.IsFormed || !_slowMa.IsFormed || !_rsi.IsFormed)
return;
if (_pairDirection != 0)
return;
if (!_hasHedgeClose)
return;
if (slowValue > fastValue && rsiValue <= OversoldLevel)
{
OpenPair(1);
}
else if (slowValue < fastValue && rsiValue >= OverboughtLevel)
{
OpenPair(-1);
}
}
private void ProcessHedgeCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
_hedgeLastClose = candle.ClosePrice;
_hasHedgeClose = true;
CheckProfitTarget();
}
private void OpenPair(int direction)
{
if (direction == 0)
return;
var basePos = GetPositionValue(Security, Portfolio) ?? 0m;
var hedgePos = GetPositionValue(HedgeSecurity, Portfolio) ?? 0m;
if (basePos != 0m || hedgePos != 0m)
return;
var volume = Volume;
if (direction > 0)
{
BuyMarket(volume, Security);
BuyMarket(volume, HedgeSecurity);
}
else
{
SellMarket(volume, Security);
SellMarket(volume, HedgeSecurity);
}
_pairDirection = direction;
_baseEntryPrice = _baseLastClose;
_hedgeEntryPrice = _hedgeLastClose;
}
private void CheckProfitTarget()
{
if (_pairDirection == 0 || !_hasBaseClose || !_hasHedgeClose)
return;
var baseProfit = _pairDirection > 0
? (_baseLastClose - _baseEntryPrice) * Volume
: (_baseEntryPrice - _baseLastClose) * Volume;
var hedgeProfit = _pairDirection > 0
? (_hedgeLastClose - _hedgeEntryPrice) * Volume
: (_hedgeEntryPrice - _hedgeLastClose) * Volume;
var totalProfit = baseProfit + hedgeProfit;
if (totalProfit >= ProfitTarget)
{
ClosePair();
}
}
private void ClosePair()
{
var basePos = GetPositionValue(Security, Portfolio) ?? 0m;
if (basePos > 0)
{
SellMarket(basePos, Security);
}
else if (basePos < 0)
{
BuyMarket(-basePos, Security);
}
var hedgePos = GetPositionValue(HedgeSecurity, Portfolio) ?? 0m;
if (hedgePos > 0)
{
SellMarket(hedgePos, HedgeSecurity);
}
else if (hedgePos < 0)
{
BuyMarket(-hedgePos, HedgeSecurity);
}
_pairDirection = 0;
_baseEntryPrice = 0m;
_hedgeEntryPrice = 0m;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from StockSharp.Algo.Indicators import SmoothedMovingAverage, RelativeStrengthIndex
from StockSharp.Algo.Strategies import Strategy
from StockSharp.Messages import DataType
from System import TimeSpan
class improve_ma_rsi_hedge_strategy(Strategy):
def __init__(self):
super(improve_ma_rsi_hedge_strategy, self).__init__()
self._profit_target = self.Param("ProfitTarget", 50.0)
self._fast_period = self.Param("FastMaPeriod", 8)
self._slow_period = self.Param("SlowMaPeriod", 21)
self._rsi_period = self.Param("RsiPeriod", 21)
self._oversold_level = self.Param("OversoldLevel", 30.0)
self._overbought_level = self.Param("OverboughtLevel", 70.0)
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(1)))
self._fast_ma = None
self._slow_ma = None
self._rsi = None
self._base_last_close = 0.0
self._base_entry_price = 0.0
self._pair_direction = 0
@property
def ProfitTarget(self):
return self._profit_target.Value
@property
def FastMaPeriod(self):
return self._fast_period.Value
@property
def SlowMaPeriod(self):
return self._slow_period.Value
@property
def RsiPeriod(self):
return self._rsi_period.Value
@property
def OversoldLevel(self):
return self._oversold_level.Value
@property
def OverboughtLevel(self):
return self._overbought_level.Value
@property
def CandleType(self):
return self._candle_type.Value
def OnStarted2(self, time):
super(improve_ma_rsi_hedge_strategy, self).OnStarted2(time)
self._fast_ma = SmoothedMovingAverage()
self._fast_ma.Length = self.FastMaPeriod
self._slow_ma = SmoothedMovingAverage()
self._slow_ma.Length = self.SlowMaPeriod
self._rsi = RelativeStrengthIndex()
self._rsi.Length = self.RsiPeriod
sub = self.SubscribeCandles(self.CandleType)
sub.Bind(self._fast_ma, self._slow_ma, self._rsi, self._process_candle).Start()
def _process_candle(self, candle, fast_val, slow_val, rsi_val):
fast_value = float(fast_val)
slow_value = float(slow_val)
rsi_value = float(rsi_val)
self._base_last_close = float(candle.ClosePrice)
if not self._fast_ma.IsFormed or not self._slow_ma.IsFormed or not self._rsi.IsFormed:
return
if self._pair_direction != 0:
if self._pair_direction > 0:
pnl = (self._base_last_close - self._base_entry_price) * float(self.Volume)
else:
pnl = (self._base_entry_price - self._base_last_close) * float(self.Volume)
if pnl >= self.ProfitTarget:
if self.Position > 0:
self.SellMarket(self.Position)
elif self.Position < 0:
self.BuyMarket(abs(self.Position))
self._pair_direction = 0
self._base_entry_price = 0.0
return
if slow_value > fast_value and rsi_value <= self.OversoldLevel:
self.BuyMarket()
self._pair_direction = 1
self._base_entry_price = self._base_last_close
elif slow_value < fast_value and rsi_value >= self.OverboughtLevel:
self.SellMarket()
self._pair_direction = -1
self._base_entry_price = self._base_last_close
def OnReseted(self):
super(improve_ma_rsi_hedge_strategy, self).OnReseted()
self._fast_ma = None
self._slow_ma = None
self._rsi = None
self._base_last_close = 0.0
self._base_entry_price = 0.0
self._pair_direction = 0
def CreateClone(self):
return improve_ma_rsi_hedge_strategy()