在 GitHub 上查看
Aussie Surfer Ltd 策略
概述
Aussie Surfer Ltd 策略 是将 MetaTrader 5 专家顾问 “Aussie Surfer Ltd”(位于 MQL 目录 43278)完整移植到 StockSharp 高层 API 的版本。策略结合了快速的布林带反弹信号与 Alligator 趋势过滤器,自动化原 EA 的主观交易思路。默认在 15 分钟 K 线序列上评估并在所选交易品种上执行市价单。
指标与数据
- 布林带(收盘价,长度 5,标准差 2.5):捕捉价格短暂冲出带外后迅速回归的形态。
- 平滑移动平均(长度 21):重建 Alligator 的“牙齿”线用于判断趋势是否减速。
- K 线中值 ((High + Low) / 2):作为 Alligator 计算的输入,确保斜率与原版 EA 保持一致。
策略仅订阅一条 K 线序列,所有信号都基于已完成的 K 线生成,从而避免未完成数据带来的噪音。
交易逻辑
- 入场条件
- 如果上一根 K 线开盘价高于其下轨,而当前 K 线开盘价跌破两根之前记录的下轨,则在平掉空头后开多头。这还原了原策略中“刺破下轨后立即回到带内”的做多逻辑。
- 如果上一根 K 线开盘价低于其上轨,而当前 K 线开盘价突破两根之前记录的上轨,则在平掉多头后开空头。
- Alligator 退出
- 追踪 Alligator 牙齿线在前两根 K 线的取值。当两根之前的数值大于一根之前的数值时说明斜率转为向下,此时平多;相反当斜率转为向上时平空。
- 风控层
- 入场时应用固定的止损和止盈(以点数表示),将点值转换为价格差。如果参数为零则禁用对应功能。
- 可选的移动止损在止损激活时启动,以前一根完成 K 线的最高价/最低价减去或加上点差距离来更新止损价位。
风险管理
- 止损:根据合约的
PriceStep 将点数转换为价格并记录初始止损。
- 止盈:在入场时计算一次并保持不变,直到触发或被其他规则提前退出。
- 移动止损:当上一根 K 线创出更高高点(多头)或更低低点(空头)时,提高或降低止损位。
- 反向处理:若出现反向信号,会发送足够的市价单一次性平掉旧方向并建立新仓位。
参数
| 参数 |
说明 |
默认值 |
OrderVolume |
每次交易的基础手数或合约数。 |
0.30 |
StopLossPips |
止损点数,0 表示禁用。 |
46 |
TakeProfitPips |
止盈点数,0 表示禁用。 |
0 |
EnableTrailingStop |
止损启用时是否跟随移动。 |
true |
BollingerPeriod |
布林带计算长度。 |
5 |
BollingerDeviation |
布林带标准差倍数。 |
2.5 |
TeethPeriod |
Alligator 牙齿线的平滑平均长度。 |
21 |
CandleType |
使用的 K 线类型(默认 15 分钟)。 |
15m K 线 |
所有数值型参数都附带优化范围,可在策略分析器中微调。
实现说明
- 仅处理完成的 K 线,以模拟原 EA 在新 K 线开始时才执行的行为。
- 如果启用移动止损但未设置正数止损距离,初始化阶段会抛出异常,提醒用户修正配置。
- 若图表区域可用,策略会自动绘制 K 线、布林带和 Alligator 牙齿线,便于对照验证移植效果。
使用步骤
- 在 StockSharp 终端或回测环境中加载该策略。
- 设置交易品种,并根据经纪商合约细则调整手数与点数参数。
- 启动策略后,它会订阅指定的 K 线序列,在每根完成的 K 线上评估信号并按上述规则管理持仓。
在实盘使用时,请确认经纪商支持市价单,并且品种的 PriceStep 信息完整,以确保点值转换准确。
using System;
using Ecng.Common;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Simplified from "Aussie Surfer Ltd" MetaTrader expert.
/// Uses Bollinger Band reversals with an SMA slope filter for entries.
/// </summary>
public class AussieSurferLtdStrategy : Strategy
{
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _bollingerPeriod;
private readonly StrategyParam<decimal> _bollingerWidth;
private readonly StrategyParam<int> _smaPeriod;
private ExponentialMovingAverage _bandEma;
private ExponentialMovingAverage _slopeEma;
private decimal? _prevSma;
private decimal? _prevClose;
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
public int BollingerPeriod
{
get => _bollingerPeriod.Value;
set => _bollingerPeriod.Value = value;
}
public decimal BollingerWidth
{
get => _bollingerWidth.Value;
set => _bollingerWidth.Value = value;
}
public int SmaPeriod
{
get => _smaPeriod.Value;
set => _smaPeriod.Value = value;
}
public AussieSurferLtdStrategy()
{
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(120).TimeFrame())
.SetDisplay("Candle Type", "Primary timeframe", "General");
_bollingerPeriod = Param(nameof(BollingerPeriod), 20)
.SetGreaterThanZero()
.SetDisplay("Bollinger Period", "Bollinger Bands window", "Indicators");
_bollingerWidth = Param(nameof(BollingerWidth), 2.5m)
.SetGreaterThanZero()
.SetDisplay("Bollinger Width", "Standard deviation multiplier", "Indicators");
_smaPeriod = Param(nameof(SmaPeriod), 21)
.SetGreaterThanZero()
.SetDisplay("SMA Period", "SMA period for slope filter", "Indicators");
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_bandEma = new ExponentialMovingAverage { Length = BollingerPeriod };
_slopeEma = new ExponentialMovingAverage { Length = SmaPeriod };
_prevSma = null;
_prevClose = null;
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(_bandEma, _slopeEma, ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, _bandEma);
DrawIndicator(area, _slopeEma);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle, decimal bandValue, decimal smaValue)
{
if (candle.State != CandleStates.Finished)
return;
if (!_bandEma.IsFormed || !_slopeEma.IsFormed)
{
_prevClose = candle.ClosePrice;
_prevSma = smaValue;
return;
}
var close = candle.ClosePrice;
var bandOffset = bandValue * (BollingerWidth / 100m);
var upperBand = bandValue + bandOffset;
var lowerBand = bandValue - bandOffset;
if (_prevSma is null || _prevClose is null)
{
_prevSma = smaValue;
_prevClose = close;
return;
}
var volume = Volume;
if (volume <= 0)
volume = 1;
// SMA slope (uptrend or downtrend)
var smaRising = smaValue > _prevSma.Value;
var smaFalling = smaValue < _prevSma.Value;
// Long: price was below lower band and crosses back above, SMA falling (reversal)
var longSignal = _prevClose.Value < lowerBand && close >= lowerBand && smaFalling;
// Short: price was above upper band and crosses back below, SMA rising (reversal)
var shortSignal = _prevClose.Value > upperBand && close <= upperBand && smaRising;
if (longSignal)
{
if (Position <= 0)
BuyMarket(Position < 0 ? Math.Abs(Position) + volume : volume);
}
else if (shortSignal)
{
if (Position >= 0)
SellMarket(Position > 0 ? Math.Abs(Position) + volume : volume);
}
// Exit at opposite band or SMA reversal
if (Position > 0 && (close >= upperBand || (smaFalling && _prevSma.Value > smaValue)))
{
SellMarket(Position);
}
else if (Position < 0 && (close <= lowerBand || (smaRising && _prevSma.Value < smaValue)))
{
BuyMarket(Math.Abs(Position));
}
_prevSma = smaValue;
_prevClose = close;
}
/// <inheritdoc />
protected override void OnReseted()
{
_bandEma = null;
_slopeEma = null;
_prevSma = null;
_prevClose = null;
base.OnReseted();
}
}
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 ExponentialMovingAverage
from StockSharp.Algo.Strategies import Strategy
class aussie_surfer_ltd_strategy(Strategy):
def __init__(self):
super(aussie_surfer_ltd_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(120)))
self._bollinger_period = self.Param("BollingerPeriod", 20)
self._bollinger_width = self.Param("BollingerWidth", 2.5)
self._sma_period = self.Param("SmaPeriod", 21)
self._prev_sma = None
self._prev_close = None
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
@property
def BollingerPeriod(self):
return self._bollinger_period.Value
@BollingerPeriod.setter
def BollingerPeriod(self, value):
self._bollinger_period.Value = value
@property
def BollingerWidth(self):
return self._bollinger_width.Value
@BollingerWidth.setter
def BollingerWidth(self, value):
self._bollinger_width.Value = value
@property
def SmaPeriod(self):
return self._sma_period.Value
@SmaPeriod.setter
def SmaPeriod(self, value):
self._sma_period.Value = value
def OnReseted(self):
super(aussie_surfer_ltd_strategy, self).OnReseted()
self._prev_sma = None
self._prev_close = None
def OnStarted2(self, time):
super(aussie_surfer_ltd_strategy, self).OnStarted2(time)
self._prev_sma = None
self._prev_close = None
band_ema = ExponentialMovingAverage()
band_ema.Length = self.BollingerPeriod
slope_ema = ExponentialMovingAverage()
slope_ema.Length = self.SmaPeriod
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(band_ema, slope_ema, self._process_candle).Start()
def _process_candle(self, candle, band_value, sma_value):
if candle.State != CandleStates.Finished:
return
band_val = float(band_value)
sma_val = float(sma_value)
close = float(candle.ClosePrice)
band_offset = band_val * (float(self.BollingerWidth) / 100.0)
upper_band = band_val + band_offset
lower_band = band_val - band_offset
if self._prev_sma is None or self._prev_close is None:
self._prev_sma = sma_val
self._prev_close = close
return
sma_rising = sma_val > self._prev_sma
sma_falling = sma_val < self._prev_sma
# Long: price was below lower band and crosses back above, SMA falling (reversal)
long_signal = self._prev_close < lower_band and close >= lower_band and sma_falling
# Short: price was above upper band and crosses back below, SMA rising (reversal)
short_signal = self._prev_close > upper_band and close <= upper_band and sma_rising
if long_signal:
if self.Position <= 0:
self.BuyMarket()
elif short_signal:
if self.Position >= 0:
self.SellMarket()
# Exit at opposite band or SMA reversal
if self.Position > 0 and (close >= upper_band or (sma_falling and self._prev_sma > sma_val)):
self.SellMarket()
elif self.Position < 0 and (close <= lower_band or (sma_rising and self._prev_sma < sma_val)):
self.BuyMarket()
self._prev_sma = sma_val
self._prev_close = close
def CreateClone(self):
return aussie_surfer_ltd_strategy()