在 GitHub 上查看

抛物线转向 Bug 2 策略

概览

抛物线转向 Bug 2 策略 是将 MetaTrader 专家顾问 pSAR_bug2(位于 MQL/9503 目录)移植到 StockSharp 高层 API 的结果。原始 EA 在抛物线 SAR 第一次翻到价格另一侧时立即采取操作:当指标点跳到收盘价下方时,系统会平掉所有空头并开立多头;当指标点跳到收盘价上方时,则执行相反的逻辑。止损和止盈距离都以价格点为单位计算,与 MetaTrader 中使用 Point 值的方式完全一致。

StockSharp 版本保持了相同的交易思想,同时利用框架提供的高层工具。策略订阅完成的蜡烛线,绑定可配置加速参数的抛物线 SAR 指标,监控指示点的翻转,并发送市价单,在一次操作中同时平掉旧仓位并建立新仓位。

交易逻辑

  1. 指标准备。策略订阅用户自定义的蜡烛类型(默认 15 分钟)并绑定抛物线 SAR 指标,使用 SarStep 作为初始加速因子、SarMaximum 作为最大加速因子。
  2. 状态跟踪。在第一根完成的蜡烛上记录 SAR 值位于收盘价上方还是下方。之后的每根蜡烛都会将新的 SAR 位置与之前的状态进行比较。
  3. 入场规则
    • 多头入场:当 SAR 从收盘价上方翻到下方时触发。下单数量为 TradeVolume + |Position|,即在同一张市价单中同时平掉现有空头并建立新的多头。入场后按照蜡烛收盘价保存止损与止盈水平。
    • 空头入场:当 SAR 从收盘价下方翻到上方时触发。任何现有多头仓位都会被在同一张市价单中平掉并开出新的空头。
  4. 保护性退出。每根完成的蜡烛都会检查保存的止损与止盈水平是否被最高价或最低价触及。一旦价格突破保护水平,策略即发送市价单平掉当前仓位并重置缓存的止损、止盈数值。

风险管理

  • 止损和止盈距离以价格点为单位,并乘以品种的最小报价步长。若交易品种未提供步长,系统会回退到 0.0001 以避免零距离。
  • 下单前会调用 IsFormedAndOnlineAndAllowTrading(),确保行情在线且允许交易。
  • 反向入场始终包含当前仓位的绝对值,保证新订单会先平掉旧仓位再建立新的方向。

参数

名称 默认值 说明
TradeVolume 0.1 下单手数,并同步写入策略基类的 Volume 属性。
StopLossPoints 90 止损距离(价格点),会乘以品种的价格步长转换为实际价格差。
TakeProfitPoints 20 止盈距离(价格点),同样通过价格步长转换。
SarStep 0.001 抛物线 SAR 的初始加速因子。
SarMaximum 0.2 抛物线 SAR 的最大加速因子。
CandleType 15 分钟周期 用于计算与信号评估的蜡烛类型。

转换说明

  • MetaTrader 中的服务器端止损/止盈通过监控蜡烛最高价与最低价并在突破时发送市价单来模拟。
  • 原 EA 依赖 OrdersTotal()OrderClose() 手动管理仓位,移植版本则通过发送一张数量为 TradeVolume + |Position| 的市价单来同时平掉旧仓位并建立新仓位。
  • 按照任务要求,目前仅提供 C# 实现,未包含 Python 版本或对应的目录。
using System;

using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Parabolic SAR Bug2: EMA crossover with RSI filter and ATR stops.
/// </summary>
public class ParabolicSarBug2Strategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _fastEmaLength;
	private readonly StrategyParam<int> _slowEmaLength;
	private readonly StrategyParam<int> _rsiLength;
	private readonly StrategyParam<int> _atrLength;

	private decimal _prevFast;
	private decimal _prevSlow;
	private decimal _entryPrice;

	public ParabolicSarBug2Strategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle Type", "Timeframe.", "General");
		_fastEmaLength = Param(nameof(FastEmaLength), 10)
			.SetDisplay("Fast EMA", "Fast EMA period.", "Indicators");
		_slowEmaLength = Param(nameof(SlowEmaLength), 30)
			.SetDisplay("Slow EMA", "Slow EMA period.", "Indicators");
		_rsiLength = Param(nameof(RsiLength), 14)
			.SetDisplay("RSI Length", "RSI period.", "Indicators");
		_atrLength = Param(nameof(AtrLength), 14)
			.SetDisplay("ATR Length", "ATR period.", "Indicators");
	}

	public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
	public int FastEmaLength { get => _fastEmaLength.Value; set => _fastEmaLength.Value = value; }
	public int SlowEmaLength { get => _slowEmaLength.Value; set => _slowEmaLength.Value = value; }
	public int RsiLength { get => _rsiLength.Value; set => _rsiLength.Value = value; }
	public int AtrLength { get => _atrLength.Value; set => _atrLength.Value = value; }

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();

		_prevFast = 0; _prevSlow = 0; _entryPrice = 0;
	}

		protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);
		_prevFast = 0; _prevSlow = 0; _entryPrice = 0;

		var fastEma = new ExponentialMovingAverage { Length = FastEmaLength };
		var slowEma = new ExponentialMovingAverage { Length = SlowEmaLength };
		var rsi = new RelativeStrengthIndex { Length = RsiLength };
		var atr = new AverageTrueRange { Length = AtrLength };

		var subscription = SubscribeCandles(CandleType);
		subscription.Bind(fastEma, slowEma, rsi, atr, ProcessCandle).Start();

		var area = CreateChartArea();
		if (area != null) { DrawCandles(area, subscription); DrawIndicator(area, fastEma); DrawIndicator(area, slowEma); DrawOwnTrades(area); }
	}

	private void ProcessCandle(ICandleMessage candle, decimal fastVal, decimal slowVal, decimal rsiVal, decimal atrVal)
	{
		if (candle.State != CandleStates.Finished) return;
		if (_prevFast == 0 || _prevSlow == 0 || atrVal <= 0) { _prevFast = fastVal; _prevSlow = slowVal; return; }
		var close = candle.ClosePrice;

		if (Position > 0)
		{
			if ((fastVal < slowVal && _prevFast >= _prevSlow) || close <= _entryPrice - atrVal * 2m) { SellMarket(); _entryPrice = 0; }
		}
		else if (Position < 0)
		{
			if ((fastVal > slowVal && _prevFast <= _prevSlow) || close >= _entryPrice + atrVal * 2m) { BuyMarket(); _entryPrice = 0; }
		}

		if (Position == 0)
		{
			if (fastVal > slowVal && _prevFast <= _prevSlow && rsiVal > 50) { _entryPrice = close; BuyMarket(); }
			else if (fastVal < slowVal && _prevFast >= _prevSlow && rsiVal < 50) { _entryPrice = close; SellMarket(); }
		}

		_prevFast = fastVal; _prevSlow = slowVal;
	}
}