Surfing 3.0 策略
概述
该 C# 策略忠实移植自 MetaTrader 4 专家顾问 Surfing 3.0。它监控由蜡烛最高价和最低价构成的指数移动平均线(EMA)通道,只要上一根蜡烛仍位于通道内部,而最新收盘价突破上下边界,就按突破方向开仓。移植版完全依赖 StockSharp 的高级 API、蜡烛订阅以及内置指标,而不是手动维护缓存数组。
算法只处理选定聚合周期的已收盘蜡烛,并保留最少量的历史数据来模拟原始代码中的 iMA 与 iClose 调用。每根蜡烛只计算一次信号,这与 MQL 版本“以收盘价决策”的风格保持一致。
指标
- 最高价 EMA / 最低价 EMA – 以蜡烛最高价和最低价为输入的两条指数移动平均线,形成一个动态通道,用于判断向上或向下突破。
- 相对强弱指数 (RSI) – 趋势过滤器。只有当 RSI 高于
LongRsiThreshold时才允许做多,低于ShortRsiThreshold时才允许做空。
交易规则
- 订阅
CandleType指定的蜡烛,并在每根收盘蜡烛上更新 EMA 与 RSI 指标。 - 保存上一根蜡烛的收盘价以及对应的 EMA 数值,它们分别对应原始专家中的
PriceClose_2、PriceHigh_2和PriceLow_2。 - 当最新收盘价 (
PriceClose_1) 向上 突破最高价 EMA,且上一根收盘价位于通道内部或边界,同时 RSI 满足多头阈值时:- 平掉现有的空头仓位(如存在)。
- 以
OrderVolume的数量买入做多。 - 按照点数计算止损与止盈价格。
- 当最新收盘价 向下 突破最低价 EMA,且上一根收盘价位于通道内部或边界,同时 RSI 低于空头阈值时:
- 平掉现有的多头仓位。
- 以
OrderVolume的数量卖出做空。 - 套用相同点数距离的保护性止损与止盈。
- 同一时间只保持一个净头寸。反向信号会先平仓再开立反方向头寸。
- 在
[TradeStartHour, TradeEndHour)交易时段之外不会开新仓。一旦时间达到TradeEndHour,策略会强制平仓并清空内部历史,重现 MQL 中closeAllPos()的行为。
风险控制
- 止损 / 止盈 – 使用合约最小变动价位(Price Step)将点数距离转换为绝对价格。设置为
0即可关闭对应的保护水平。 - 交易时段平仓 – 当达到
TradeEndHour时立即平仓并清除目标价,避免头寸隔夜,完全符合原策略的交易时间限制。
参数
| 名称 | 说明 | 默认值 |
|---|---|---|
OrderVolume |
每笔市场订单的交易量。 | 1 |
TakeProfitPoints |
止盈距离(点数)。 | 80 |
StopLossPoints |
止损距离(点数)。 | 50 |
MaPeriod |
计算高低价 EMA 的周期。 | 50 |
RsiPeriod |
RSI 指标的周期长度。 | 10 |
LongRsiThreshold |
允许做多所需的最小 RSI 值。 | 40 |
ShortRsiThreshold |
允许做空所需的最大 RSI 值。 | 65 |
TradeStartHour |
允许开仓的起始小时(交易所时间)。 | 8 |
TradeEndHour |
平仓并停止开仓的小时(不含该刻)。 | 18 |
CandleType |
用于计算的蜡烛聚合方式(默认 15 分钟)。 | 15m |
说明
- 仅基于已收盘蜡烛计算信号,忽略盘中波动,与 MetaTrader 中的行为一致。
- 交易时段结束后会重置 EMA 历史,避免不同交易日之间的数据混淆。
- 根据项目要求,本策略未提供 Python 版本。
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;
using StockSharp.Algo.Candles;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Strategy that reproduces the Surfing 3.0 expert advisor logic from MetaTrader.
/// </summary>
public class Surfing30Strategy : Strategy
{
private readonly StrategyParam<decimal> _orderVolume;
private readonly StrategyParam<int> _takeProfitPoints;
private readonly StrategyParam<int> _stopLossPoints;
private readonly StrategyParam<int> _maPeriod;
private readonly StrategyParam<int> _rsiPeriod;
private readonly StrategyParam<decimal> _longRsiThreshold;
private readonly StrategyParam<decimal> _shortRsiThreshold;
private readonly StrategyParam<int> _tradeStartHour;
private readonly StrategyParam<int> _tradeEndHour;
private readonly StrategyParam<DataType> _candleType;
private RelativeStrengthIndex _rsi = null!;
private decimal? _previousClose;
private decimal? _previousHighEma;
private decimal? _previousLowEma;
private decimal? _stopLossPrice;
private decimal? _takeProfitPrice;
/// <summary>
/// Initialize <see cref="Surfing30Strategy"/>.
/// </summary>
public Surfing30Strategy()
{
_orderVolume = Param(nameof(OrderVolume), 1m)
.SetGreaterThanZero()
.SetDisplay("Order Volume", "Volume applied to every trade.", "Trading")
.SetOptimize(0.1m, 5m, 0.1m);
_takeProfitPoints = Param(nameof(TakeProfitPoints), 80)
.SetNotNegative()
.SetDisplay("Take Profit Points", "Distance to the take profit in instrument points.", "Risk Management")
.SetOptimize(10, 200, 10);
_stopLossPoints = Param(nameof(StopLossPoints), 50)
.SetNotNegative()
.SetDisplay("Stop Loss Points", "Distance to the stop loss in instrument points.", "Risk Management")
.SetOptimize(10, 150, 10);
_maPeriod = Param(nameof(MaPeriod), 50)
.SetRange(1, 1000)
.SetDisplay("EMA Period", "Length of the exponential moving averages calculated over highs and lows.", "Indicators")
.SetOptimize(10, 120, 5);
_rsiPeriod = Param(nameof(RsiPeriod), 10)
.SetRange(1, 1000)
.SetDisplay("RSI Period", "Length of the RSI filter.", "Indicators")
.SetOptimize(5, 30, 1);
_longRsiThreshold = Param(nameof(LongRsiThreshold), 30m)
.SetDisplay("Long RSI Threshold", "Minimum RSI value required for long entries.", "Filters")
.SetOptimize(20m, 60m, 5m);
_shortRsiThreshold = Param(nameof(ShortRsiThreshold), 70m)
.SetDisplay("Short RSI Threshold", "Maximum RSI value allowed for short entries.", "Filters")
.SetOptimize(40m, 80m, 5m);
_tradeStartHour = Param(nameof(TradeStartHour), 0)
.SetDisplay("Trade Start Hour", "Hour of the day when new trades may start.", "Sessions")
;
_tradeEndHour = Param(nameof(TradeEndHour), 23)
.SetDisplay("Trade End Hour", "Hour of the day when all positions are closed.", "Sessions")
;
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(15).TimeFrame())
.SetDisplay("Candle Type", "Aggregation used for calculations.", "Data");
}
/// <summary>
/// Volume used for every trade.
/// </summary>
public decimal OrderVolume
{
get => _orderVolume.Value;
set
{
_orderVolume.Value = value;
Volume = value;
}
}
/// <summary>
/// Distance to the take profit in instrument points.
/// </summary>
public int TakeProfitPoints
{
get => _takeProfitPoints.Value;
set => _takeProfitPoints.Value = value;
}
/// <summary>
/// Distance to the stop loss in instrument points.
/// </summary>
public int StopLossPoints
{
get => _stopLossPoints.Value;
set => _stopLossPoints.Value = value;
}
/// <summary>
/// Length of the exponential moving averages calculated over candle highs and lows.
/// </summary>
public int MaPeriod
{
get => _maPeriod.Value;
set => _maPeriod.Value = value;
}
/// <summary>
/// Length of the RSI filter.
/// </summary>
public int RsiPeriod
{
get => _rsiPeriod.Value;
set => _rsiPeriod.Value = value;
}
/// <summary>
/// Minimum RSI value required for long entries.
/// </summary>
public decimal LongRsiThreshold
{
get => _longRsiThreshold.Value;
set => _longRsiThreshold.Value = value;
}
/// <summary>
/// Maximum RSI value allowed for short entries.
/// </summary>
public decimal ShortRsiThreshold
{
get => _shortRsiThreshold.Value;
set => _shortRsiThreshold.Value = value;
}
/// <summary>
/// Hour of the day when new trades may start.
/// </summary>
public int TradeStartHour
{
get => _tradeStartHour.Value;
set => _tradeStartHour.Value = value;
}
/// <summary>
/// Hour of the day when all positions are closed.
/// </summary>
public int TradeEndHour
{
get => _tradeEndHour.Value;
set => _tradeEndHour.Value = value;
}
/// <summary>
/// Candle aggregation used for calculations.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
yield return (Security, CandleType);
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_previousClose = null;
_previousHighEma = null;
_previousLowEma = null;
_stopLossPrice = null;
_takeProfitPrice = null;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
Volume = OrderVolume;
var sma = new SimpleMovingAverage { Length = MaPeriod };
_rsi = new RelativeStrengthIndex { Length = RsiPeriod };
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(sma, _rsi, ProcessCandle)
.Start();
}
private void ProcessCandle(ICandleMessage candle, decimal smaValue, decimal rsiValue)
{
if (candle.State != CandleStates.Finished)
return;
var currentClose = candle.ClosePrice;
if (ManageActivePosition(candle))
{
UpdateHistory(currentClose, smaValue, smaValue);
return;
}
if (_previousClose is null || _previousHighEma is null)
{
UpdateHistory(currentClose, smaValue, smaValue);
return;
}
var previousClose = _previousClose.Value;
var previousSma = _previousHighEma.Value;
var buySignal = previousClose <= previousSma && currentClose > smaValue && rsiValue > LongRsiThreshold;
var sellSignal = previousClose >= previousSma && currentClose < smaValue && rsiValue < ShortRsiThreshold;
if (buySignal && Position <= 0)
{
if (Position < 0)
{
CloseCurrentPosition();
ResetTargets();
}
BuyMarket(OrderVolume);
SetTargets(currentClose, true);
}
else if (sellSignal && Position >= 0)
{
if (Position > 0)
{
CloseCurrentPosition();
ResetTargets();
}
SellMarket(OrderVolume);
SetTargets(currentClose, false);
}
UpdateHistory(currentClose, smaValue, smaValue);
}
private bool ManageActivePosition(ICandleMessage candle)
{
if (Position > 0)
{
if (_stopLossPrice is not null && candle.LowPrice <= _stopLossPrice)
{
CloseCurrentPosition();
ResetTargets();
return true;
}
if (_takeProfitPrice is not null && candle.HighPrice >= _takeProfitPrice)
{
CloseCurrentPosition();
ResetTargets();
return true;
}
}
else if (Position < 0)
{
if (_stopLossPrice is not null && candle.HighPrice >= _stopLossPrice)
{
CloseCurrentPosition();
ResetTargets();
return true;
}
if (_takeProfitPrice is not null && candle.LowPrice <= _takeProfitPrice)
{
CloseCurrentPosition();
ResetTargets();
return true;
}
}
return false;
}
private void CloseCurrentPosition()
{
if (Position > 0m)
SellMarket(Position);
else if (Position < 0m)
BuyMarket(Math.Abs(Position));
}
private void SetTargets(decimal entryPrice, bool isLong)
{
var priceStep = Security?.PriceStep ?? 0m;
if (priceStep <= 0m)
priceStep = 1m;
if (isLong)
{
_stopLossPrice = StopLossPoints > 0 ? entryPrice - StopLossPoints * priceStep : null;
_takeProfitPrice = TakeProfitPoints > 0 ? entryPrice + TakeProfitPoints * priceStep : null;
}
else
{
_stopLossPrice = StopLossPoints > 0 ? entryPrice + StopLossPoints * priceStep : null;
_takeProfitPrice = TakeProfitPoints > 0 ? entryPrice - TakeProfitPoints * priceStep : null;
}
}
private void ResetTargets()
{
_stopLossPrice = null;
_takeProfitPrice = null;
}
private void UpdateHistory(decimal currentClose, decimal currentHighEma, decimal currentLowEma)
{
_previousClose = currentClose;
_previousHighEma = currentHighEma;
_previousLowEma = currentLowEma;
}
private void ResetHistory()
{
_previousClose = null;
_previousHighEma = null;
_previousLowEma = null;
}
private bool IsWithinTradeHours(DateTimeOffset time)
{
var startHour = TradeStartHour;
var endHour = TradeEndHour;
if (endHour <= startHour)
return time.Hour >= startHour || time.Hour < endHour;
return time.Hour >= startHour && time.Hour < endHour;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import Math, TimeSpan
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Indicators import SimpleMovingAverage, RelativeStrengthIndex
from StockSharp.Algo.Strategies import Strategy
class surfing_30_strategy(Strategy):
def __init__(self):
super(surfing_30_strategy, self).__init__()
self._order_volume = self.Param("OrderVolume", 1.0)
self._tp_points = self.Param("TakeProfitPoints", 80)
self._sl_points = self.Param("StopLossPoints", 50)
self._ma_period = self.Param("MaPeriod", 50)
self._rsi_period = self.Param("RsiPeriod", 10)
self._long_rsi = self.Param("LongRsiThreshold", 30.0)
self._short_rsi = self.Param("ShortRsiThreshold", 70.0)
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(15)))
self._prev_close = None
self._prev_sma = None
self._sl_price = None
self._tp_price = None
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
@property
def OrderVolume(self):
return self._order_volume.Value
@OrderVolume.setter
def OrderVolume(self, value):
self._order_volume.Value = value
def OnReseted(self):
super(surfing_30_strategy, self).OnReseted()
self._prev_close = None
self._prev_sma = None
self._sl_price = None
self._tp_price = None
def OnStarted2(self, time):
super(surfing_30_strategy, self).OnStarted2(time)
self.Volume = float(self.OrderVolume)
sma = SimpleMovingAverage()
sma.Length = self._ma_period.Value
rsi = RelativeStrengthIndex()
rsi.Length = self._rsi_period.Value
sub = self.SubscribeCandles(self.CandleType)
sub.Bind(sma, rsi, self._process_candle).Start()
def _close_current_position(self):
if self.Position > 0:
self.SellMarket(float(self.Position))
elif self.Position < 0:
self.BuyMarket(abs(float(self.Position)))
def _set_targets(self, entry_price, is_long):
price_step = 1.0
if self.Security is not None and self.Security.PriceStep is not None and float(self.Security.PriceStep) > 0:
price_step = float(self.Security.PriceStep)
sl_pts = int(self._sl_points.Value)
tp_pts = int(self._tp_points.Value)
if is_long:
self._sl_price = entry_price - sl_pts * price_step if sl_pts > 0 else None
self._tp_price = entry_price + tp_pts * price_step if tp_pts > 0 else None
else:
self._sl_price = entry_price + sl_pts * price_step if sl_pts > 0 else None
self._tp_price = entry_price - tp_pts * price_step if tp_pts > 0 else None
def _reset_targets(self):
self._sl_price = None
self._tp_price = None
def _manage_active_position(self, candle):
if self.Position > 0:
if self._sl_price is not None and float(candle.LowPrice) <= self._sl_price:
self._close_current_position()
self._reset_targets()
return True
if self._tp_price is not None and float(candle.HighPrice) >= self._tp_price:
self._close_current_position()
self._reset_targets()
return True
elif self.Position < 0:
if self._sl_price is not None and float(candle.HighPrice) >= self._sl_price:
self._close_current_position()
self._reset_targets()
return True
if self._tp_price is not None and float(candle.LowPrice) <= self._tp_price:
self._close_current_position()
self._reset_targets()
return True
return False
def _process_candle(self, candle, sma_val, rsi_val):
if candle.State != CandleStates.Finished:
return
close = float(candle.ClosePrice)
sma_v = float(sma_val)
rsi_v = float(rsi_val)
if self._manage_active_position(candle):
self._prev_close = close
self._prev_sma = sma_v
return
if self._prev_close is None or self._prev_sma is None:
self._prev_close = close
self._prev_sma = sma_v
return
prev_close = self._prev_close
prev_sma = self._prev_sma
buy_signal = prev_close <= prev_sma and close > sma_v and rsi_v > float(self._long_rsi.Value)
sell_signal = prev_close >= prev_sma and close < sma_v and rsi_v < float(self._short_rsi.Value)
if buy_signal and self.Position <= 0:
if self.Position < 0:
self._close_current_position()
self._reset_targets()
self.BuyMarket(float(self.OrderVolume))
self._set_targets(close, True)
elif sell_signal and self.Position >= 0:
if self.Position > 0:
self._close_current_position()
self._reset_targets()
self.SellMarket(float(self.OrderVolume))
self._set_targets(close, False)
self._prev_close = close
self._prev_sma = sma_v
def CreateClone(self):
return surfing_30_strategy()