Hpcs Inter6 RSI 策略
概述
Hpcs Inter6 RSI 策略将 MetaTrader 专家顾问 _HPCS_Inter6_MT4_EA_V01_WE 移植到 StockSharp 高级 API。算法观察可配置周期的相对强弱指标(RSI),关注围绕经典 70/30 阈值的快速反转。当 RSI 向上穿越 70 时策略转为空头,当 RSI 向下穿越 30 时策略转为多头。每次入场都会立即设置对称的止盈和止损,距离以点(pip)表示。
数据与指标
- K线来源:用户可配置的时间框架(默认 1 小时)。
- 指标:可配置周期的相对强弱指标(默认 14),通过 StockSharp 指标绑定流水线重新计算。
入场逻辑
- 策略只在 K 线收盘后工作,避免使用未完成的数据。
- 每根收盘 K 线比较新的 RSI 数值与前一根的数值。
- 做空条件:若 RSI 刚从下方穿越
UpperLevel(默认 70),策略以市价卖出。若存在多头头寸,会先平掉多头,再建立与配置交易量相等的净空头。 - 做多条件:若 RSI 刚从上方穿越
LowerLevel(默认 30),策略以市价买入。若存在空头头寸,会先回补空头,再建立与配置交易量相等的净多头。 - 每根 K 线最多触发一次信号;同一根 K 线上的重复信号会被忽略,以匹配原始 EA 使用 K 线时间戳的保护机制。
离场逻辑
- 每次入场都会设置相同距离的止盈和止损。
- 持有多头时,如果 K 线最高价触及止盈或最低价触及止损,立即平仓。
- 持有空头时,如果 K 线最低价触及止盈或最高价触及止损,立即平仓。
- 空仓时会清除所有保护价位。
点距会根据交易标的的最小报价单位进行换算。若标的使用三位或五位小数,算法会将距离乘以 10,以符合 MetaTrader 中对 1 点(pip)的定义。
参数
| 参数 | 默认值 | 说明 |
|---|---|---|
CandleType |
1 小时时间框架 | 提供 RSI 计算所需的 K 线。 |
RsiLength |
14 | RSI 的回溯周期。 |
UpperLevel |
70 | RSI 向上穿越该值时触发做空。 |
LowerLevel |
30 | RSI 向下穿越该值时触发做多。 |
TradeVolume |
1 | 市价单的下单手数,反向持仓会在入场前平掉。 |
OffsetInPips |
10 | 止盈和止损距入场价的距离(单位:点)。 |
所有参数均通过 StrategyParam 暴露,可在 StockSharp 中执行优化。
说明
- 策略使用 K 线的最高价与最低价来模拟止盈止损的触发,贴近 MetaTrader 的固定价位行为。
- 不会挂出挂单,所有交易均通过市价单完成。
- 如果界面中存在图表区域,策略会自动绘制价格与 RSI 指标,方便监控。
using System;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Port of the MetaTrader strategy _HPCS_Inter6_MT4_EA_V01_WE.
/// Trades RSI reversals at the 70/30 levels with symmetric fixed targets and stops.
/// </summary>
public class HpcsInter6RsiStrategy : Strategy
{
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _rsiLength;
private readonly StrategyParam<decimal> _upperLevel;
private readonly StrategyParam<decimal> _lowerLevel;
private readonly StrategyParam<decimal> _tradeVolume;
private readonly StrategyParam<decimal> _offsetInPips;
private readonly StrategyParam<int> _signalCooldownCandles;
private RelativeStrengthIndex _rsi;
private decimal? _previousRsi;
private DateTimeOffset? _lastSignalTime;
private decimal? _targetPrice;
private decimal? _stopPrice;
private bool _isLongPosition;
private int _candlesSinceTrade;
/// <summary>
/// Candle type used for the RSI evaluation.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// RSI lookback length.
/// </summary>
public int RsiLength
{
get => _rsiLength.Value;
set => _rsiLength.Value = value;
}
/// <summary>
/// Upper RSI level that triggers short entries when crossed from below.
/// </summary>
public decimal UpperLevel
{
get => _upperLevel.Value;
set => _upperLevel.Value = value;
}
/// <summary>
/// Lower RSI level that triggers long entries when crossed from above.
/// </summary>
public decimal LowerLevel
{
get => _lowerLevel.Value;
set => _lowerLevel.Value = value;
}
/// <summary>
/// Market order volume used for entries.
/// </summary>
public decimal TradeVolume
{
get => _tradeVolume.Value;
set => _tradeVolume.Value = value;
}
/// <summary>
/// Target and stop distance expressed in pips.
/// </summary>
public decimal OffsetInPips
{
get => _offsetInPips.Value;
set => _offsetInPips.Value = value;
}
public int SignalCooldownCandles
{
get => _signalCooldownCandles.Value;
set => _signalCooldownCandles.Value = value;
}
/// <summary>
/// Initializes a new instance of the <see cref="HpcsInter6RsiStrategy"/> class.
/// </summary>
public HpcsInter6RsiStrategy()
{
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(60).TimeFrame())
.SetDisplay("Candle Type", "Time frame for RSI evaluation", "General");
_rsiLength = Param(nameof(RsiLength), 7)
.SetGreaterThanZero()
.SetDisplay("RSI Length", "Lookback period for RSI", "Parameters")
.SetOptimize(5, 40, 1);
_upperLevel = Param(nameof(UpperLevel), 65m)
.SetDisplay("Upper RSI", "Upper RSI level for shorts", "Parameters")
.SetOptimize(60m, 90m, 5m);
_lowerLevel = Param(nameof(LowerLevel), 35m)
.SetDisplay("Lower RSI", "Lower RSI level for longs", "Parameters")
.SetOptimize(10m, 40m, 5m);
_tradeVolume = Param(nameof(TradeVolume), 1m)
.SetGreaterThanZero()
.SetDisplay("Trade Volume", "Order volume for entries", "Trading")
.SetOptimize(0.1m, 5m, 0.1m);
_offsetInPips = Param(nameof(OffsetInPips), 30m)
.SetGreaterThanZero()
.SetDisplay("Offset (pips)", "Target and stop distance in pips", "Risk")
.SetOptimize(5m, 30m, 5m);
_signalCooldownCandles = Param(nameof(SignalCooldownCandles), 4)
.SetGreaterThanZero()
.SetDisplay("Signal Cooldown", "Bars to wait between entries", "Trading");
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_rsi = null;
_previousRsi = null;
_lastSignalTime = null;
_targetPrice = null;
_stopPrice = null;
_isLongPosition = false;
_candlesSinceTrade = SignalCooldownCandles;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_previousRsi = null;
_lastSignalTime = null;
_targetPrice = null;
_stopPrice = null;
_isLongPosition = false;
_candlesSinceTrade = SignalCooldownCandles;
_rsi = new RelativeStrengthIndex
{
Length = RsiLength
};
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(_rsi, ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, _rsi);
}
}
private void ProcessCandle(ICandleMessage candle, decimal rsiValue)
{
if (candle.State != CandleStates.Finished)
return;
if (!_rsi.IsFormed)
return;
if (_candlesSinceTrade < SignalCooldownCandles)
_candlesSinceTrade++;
UpdateActivePositionTargets(candle);
var previousRsi = _previousRsi;
_previousRsi = rsiValue;
if (previousRsi is null)
return;
var candleTime = candle.OpenTime;
if (_lastSignalTime.HasValue && _lastSignalTime.Value == candleTime)
return;
if (_candlesSinceTrade >= SignalCooldownCandles && TryEnterShort(candle, rsiValue, previousRsi.Value))
{
_lastSignalTime = candleTime;
_candlesSinceTrade = 0;
return;
}
if (_candlesSinceTrade >= SignalCooldownCandles && TryEnterLong(candle, rsiValue, previousRsi.Value))
{
_lastSignalTime = candleTime;
_candlesSinceTrade = 0;
}
}
private void UpdateActivePositionTargets(ICandleMessage candle)
{
if (Position > 0)
{
if (!_isLongPosition)
{
_targetPrice = null;
_stopPrice = null;
return;
}
var shouldExit = (_targetPrice.HasValue && candle.HighPrice >= _targetPrice.Value)
|| (_stopPrice.HasValue && candle.LowPrice <= _stopPrice.Value);
if (shouldExit)
{
SellMarket(Math.Abs(Position));
_targetPrice = null;
_stopPrice = null;
}
}
else if (Position < 0)
{
if (_isLongPosition)
{
_targetPrice = null;
_stopPrice = null;
return;
}
var shouldExit = (_targetPrice.HasValue && candle.LowPrice <= _targetPrice.Value)
|| (_stopPrice.HasValue && candle.HighPrice >= _stopPrice.Value);
if (shouldExit)
{
BuyMarket(Math.Abs(Position));
_targetPrice = null;
_stopPrice = null;
}
}
else
{
_targetPrice = null;
_stopPrice = null;
_isLongPosition = false;
}
}
private bool TryEnterShort(ICandleMessage candle, decimal currentRsi, decimal previousRsi)
{
if (!(currentRsi > UpperLevel && previousRsi <= UpperLevel))
return false;
var volume = TradeVolume;
if (volume <= 0m)
return false;
if (Position > 0)
{
volume += Math.Abs(Position);
}
SellMarket(volume);
var offset = CalculateOffset();
if (offset > 0m)
{
var entryPrice = candle.ClosePrice;
_targetPrice = entryPrice - offset;
_stopPrice = entryPrice + offset;
_isLongPosition = false;
}
else
{
_targetPrice = null;
_stopPrice = null;
_isLongPosition = false;
}
return true;
}
private bool TryEnterLong(ICandleMessage candle, decimal currentRsi, decimal previousRsi)
{
if (!(currentRsi < LowerLevel && previousRsi >= LowerLevel))
return false;
var volume = TradeVolume;
if (volume <= 0m)
return false;
if (Position < 0)
{
volume += Math.Abs(Position);
}
BuyMarket(volume);
var offset = CalculateOffset();
if (offset > 0m)
{
var entryPrice = candle.ClosePrice;
_targetPrice = entryPrice + offset;
_stopPrice = entryPrice - offset;
_isLongPosition = true;
}
else
{
_targetPrice = null;
_stopPrice = null;
_isLongPosition = true;
}
return true;
}
private decimal CalculateOffset()
{
var priceStep = Security?.PriceStep ?? 0.01m;
if (priceStep <= 0m)
priceStep = 0.01m;
var decimals = Security?.Decimals ?? 0;
var factor = decimals is 3 or 5 ? 10m : 1m;
return OffsetInPips * priceStep * factor;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Indicators import RelativeStrengthIndex
from StockSharp.Algo.Strategies import Strategy
class hpcs_inter6_rsi_strategy(Strategy):
"""
HPCS Inter6 RSI: trades RSI reversals at configurable levels.
Sells when RSI crosses above upper level, buys when crossing below lower level.
Manages SL/TP based on pip offset with signal cooldown.
"""
def __init__(self):
super(hpcs_inter6_rsi_strategy, self).__init__()
self._rsi_length = self.Param("RsiLength", 7) \
.SetDisplay("RSI Length", "Lookback period for RSI", "Parameters")
self._upper_level = self.Param("UpperLevel", 65.0) \
.SetDisplay("Upper RSI", "Upper RSI level for shorts", "Parameters")
self._lower_level = self.Param("LowerLevel", 35.0) \
.SetDisplay("Lower RSI", "Lower RSI level for longs", "Parameters")
self._offset_in_pips = self.Param("OffsetInPips", 30.0) \
.SetDisplay("Offset (pips)", "Target and stop distance in pips", "Risk")
self._signal_cooldown_candles = self.Param("SignalCooldownCandles", 4) \
.SetDisplay("Signal Cooldown", "Bars to wait between entries", "Trading")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(60))) \
.SetDisplay("Candle Type", "Time frame for RSI evaluation", "General")
self._prev_rsi = None
self._target_price = None
self._stop_price = None
self._is_long_position = False
self._candles_since_trade = 0
@property
def candle_type(self):
return self._candle_type.Value
def OnReseted(self):
super(hpcs_inter6_rsi_strategy, self).OnReseted()
self._prev_rsi = None
self._target_price = None
self._stop_price = None
self._is_long_position = False
self._candles_since_trade = self._signal_cooldown_candles.Value
def OnStarted2(self, time):
super(hpcs_inter6_rsi_strategy, self).OnStarted2(time)
self._candles_since_trade = self._signal_cooldown_candles.Value
rsi = RelativeStrengthIndex()
rsi.Length = self._rsi_length.Value
subscription = self.SubscribeCandles(self.candle_type)
subscription.Bind(rsi, self._process_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, rsi)
def _process_candle(self, candle, rsi_val):
if candle.State != CandleStates.Finished:
return
rsi = float(rsi_val)
if self._candles_since_trade < self._signal_cooldown_candles.Value:
self._candles_since_trade += 1
self._update_position_targets(candle)
prev_rsi = self._prev_rsi
self._prev_rsi = rsi
if prev_rsi is None:
return
cooldown = self._signal_cooldown_candles.Value
if self._candles_since_trade >= cooldown:
if rsi > self._upper_level.Value and prev_rsi <= self._upper_level.Value:
self._enter_short(candle)
self._candles_since_trade = 0
return
if self._candles_since_trade >= cooldown:
if rsi < self._lower_level.Value and prev_rsi >= self._lower_level.Value:
self._enter_long(candle)
self._candles_since_trade = 0
def _update_position_targets(self, candle):
high = float(candle.HighPrice)
low = float(candle.LowPrice)
if self.Position > 0:
if not self._is_long_position:
self._target_price = None
self._stop_price = None
return
should_exit = False
if self._target_price is not None and high >= self._target_price:
should_exit = True
if self._stop_price is not None and low <= self._stop_price:
should_exit = True
if should_exit:
self.SellMarket()
self._target_price = None
self._stop_price = None
elif self.Position < 0:
if self._is_long_position:
self._target_price = None
self._stop_price = None
return
should_exit = False
if self._target_price is not None and low <= self._target_price:
should_exit = True
if self._stop_price is not None and high >= self._stop_price:
should_exit = True
if should_exit:
self.BuyMarket()
self._target_price = None
self._stop_price = None
else:
self._target_price = None
self._stop_price = None
self._is_long_position = False
def _enter_short(self, candle):
if self.Position > 0:
self.SellMarket()
self.SellMarket()
offset = self._calculate_offset()
if offset > 0:
entry = float(candle.ClosePrice)
self._target_price = entry - offset
self._stop_price = entry + offset
self._is_long_position = False
else:
self._target_price = None
self._stop_price = None
self._is_long_position = False
def _enter_long(self, candle):
if self.Position < 0:
self.BuyMarket()
self.BuyMarket()
offset = self._calculate_offset()
if offset > 0:
entry = float(candle.ClosePrice)
self._target_price = entry + offset
self._stop_price = entry - offset
self._is_long_position = True
else:
self._target_price = None
self._stop_price = None
self._is_long_position = True
def _calculate_offset(self):
step = 0.01
if self.Security is not None and self.Security.PriceStep is not None:
step = float(self.Security.PriceStep)
if step <= 0:
step = 0.01
decimals = 0
if self.Security is not None and self.Security.Decimals is not None:
decimals = int(self.Security.Decimals)
factor = 10.0 if decimals in (3, 5) else 1.0
return self._offset_in_pips.Value * step * factor
def CreateClone(self):
return hpcs_inter6_rsi_strategy()