在 GitHub 上查看

AeroSpine 策略

概述

AeroSpine 策略源自 MetaTrader 专家顾问 AEROSPINE.mq4。它专注于单一品种,通过跟踪价格突破每日开盘价的行为来捕捉趋势。原始 EA 要求挂载在日线图并按 tick 运行,本移植版本保留了“日线开盘突破”思想,但通过 StockSharp 提供的已完成 K 线进行计算。

交易逻辑

  • 每个交易日开始时记录当日开盘价(取自当日第一根 K 线的开盘价)。
  • 仅在达到设定的入场小时之后才评估新仓位,同时要求最近一根完成的 K 线满足最低成交量过滤,并且当前点差不超过限制。
  • 当没有持仓且未等待补仓交易时:
    • 当 K 线最高价超过日开盘价 EntryOffsetPips 点时开多单。
    • 当 K 线最低价跌破日开盘价 EntryOffsetPips 点时开空单。
  • 任何一次交易亏损出场后,会准备在相反方向进行补仓。补仓交易使用 RecoveryOffsetPips 判断触发点,并将亏损单的手数与基础手数相加,复制原 EA 的马丁加仓逻辑。
  • 持仓后的管理包括三部分:
    • 在距离入场价 TakeProfitPips 点的位置设置固定止盈。
    • 可选的保本机制:当价格先行达到 BreakEvenPips 距离后回撤到该水平即平仓。
    • 若价格回到日开盘价并逆向突破 ExitOffsetPips 点,则立即离场。

参数

名称 说明
Candle Type 信号计算所使用的工作周期。
Volume 首次进场的基础手数,同时用于计算补仓手数。
Entry Hour 允许开仓的最小小时(交易所时间)。
Entry Offset 与日开盘价的距离(点),突破该距离后才进行首笔交易。
Exit Offset 当价格反向越过日开盘价并达到该距离时平仓。
Recovery Offset 亏损后补仓时所需的触发距离(点)。
Take Profit 入场价起算的固定止盈距离。
Break Even 触发保本逻辑所需的价格距离。
Use Break Even 是否启用保本管理。
Volume Filter 最低 K 线成交量要求,对应原版的 Volume[0] > 10000 检查。
Max Spread 点差高于该值时禁止新开仓。
Enable Recovery 是否启用亏损后的反向补仓。

移植说明

  • 原 EA 在日线图上通过 tick 判断突破,本版本改为在每个交易日的第一根 K 线上刷新日开盘价,并通过后续已完成 K 线的最高价、最低价检测突破。
  • 与图表相关的文本标签、跨品种统计等功能被移除,仅保留与当前品种相关的交易逻辑。
  • MetaTrader 中通过 OrderModify 调整止损的流程改为在触及计算出的价位时直接调用 ClosePosition() 平仓。
  • 点差和成交量过滤与原始代码的 MODE_SPREADVolume[0] 检查保持一致。
using System;
using System.Collections.Generic;

using Ecng.Common;

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

namespace StockSharp.Samples.Strategies;

public class AeroSpineStrategy : Strategy
{
	private readonly StrategyParam<int> _fastPeriod;
	private readonly StrategyParam<int> _slowPeriod;
	private readonly StrategyParam<int> _stopLossPoints;
	private readonly StrategyParam<int> _takeProfitPoints;

	private ExponentialMovingAverage _fast;
	private ExponentialMovingAverage _slow;

	private decimal _prevFast;
	private decimal _prevSlow;
	private decimal _entryPrice;
	private int _cooldown;

	public int FastPeriod { get => _fastPeriod.Value; set => _fastPeriod.Value = value; }
	public int SlowPeriod { get => _slowPeriod.Value; set => _slowPeriod.Value = value; }
	public int StopLossPoints { get => _stopLossPoints.Value; set => _stopLossPoints.Value = value; }
	public int TakeProfitPoints { get => _takeProfitPoints.Value; set => _takeProfitPoints.Value = value; }

	public AeroSpineStrategy()
	{
		_fastPeriod = Param(nameof(FastPeriod), 14).SetGreaterThanZero().SetDisplay("Fast Period", "Fast EMA period", "Indicator");
		_slowPeriod = Param(nameof(SlowPeriod), 50).SetGreaterThanZero().SetDisplay("Slow Period", "Slow EMA period", "Indicator");
		_stopLossPoints = Param(nameof(StopLossPoints), 200).SetNotNegative().SetDisplay("Stop Loss", "Stop-loss in price steps", "Risk");
		_takeProfitPoints = Param(nameof(TakeProfitPoints), 400).SetNotNegative().SetDisplay("Take Profit", "Take-profit in price steps", "Risk");
	}

	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		yield return (Security, TimeSpan.FromMinutes(5).TimeFrame());
	}

	protected override void OnReseted()
	{
		base.OnReseted();
		_fast = null; _slow = null;
		_prevFast = 0; _prevSlow = 0; _entryPrice = 0; _cooldown = 0;
	}

	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);
		_fast = new ExponentialMovingAverage { Length = FastPeriod };
		_slow = new ExponentialMovingAverage { Length = SlowPeriod };
		var subscription = SubscribeCandles(TimeSpan.FromMinutes(5).TimeFrame());
		subscription.Bind(_fast, _slow, ProcessCandle);
		subscription.Start();
	}

	private void ProcessCandle(ICandleMessage candle, decimal fastValue, decimal slowValue)
	{
		if (candle.State != CandleStates.Finished) return;
		if (!_fast.IsFormed || !_slow.IsFormed) { _prevFast = fastValue; _prevSlow = slowValue; return; }
		if (_cooldown > 0) { _cooldown--; _prevFast = fastValue; _prevSlow = slowValue; return; }

		var close = candle.ClosePrice;
		var step = Security?.PriceStep ?? 1m;

		if (Position > 0 && _entryPrice > 0)
		{
			if (StopLossPoints > 0 && close <= _entryPrice - StopLossPoints * step) { SellMarket(); _entryPrice = 0; _cooldown = 100; _prevFast = fastValue; _prevSlow = slowValue; return; }
			if (TakeProfitPoints > 0 && close >= _entryPrice + TakeProfitPoints * step) { SellMarket(); _entryPrice = 0; _cooldown = 100; _prevFast = fastValue; _prevSlow = slowValue; return; }
		}
		else if (Position < 0 && _entryPrice > 0)
		{
			if (StopLossPoints > 0 && close >= _entryPrice + StopLossPoints * step) { BuyMarket(); _entryPrice = 0; _cooldown = 100; _prevFast = fastValue; _prevSlow = slowValue; return; }
			if (TakeProfitPoints > 0 && close <= _entryPrice - TakeProfitPoints * step) { BuyMarket(); _entryPrice = 0; _cooldown = 100; _prevFast = fastValue; _prevSlow = slowValue; return; }
		}

		if (_prevFast <= _prevSlow && fastValue > slowValue && Position <= 0)
		{ if (Position < 0) BuyMarket(); BuyMarket(); _entryPrice = close; _cooldown = 100; }
		else if (_prevFast >= _prevSlow && fastValue < slowValue && Position >= 0)
		{ if (Position > 0) SellMarket(); SellMarket(); _entryPrice = close; _cooldown = 100; }

		_prevFast = fastValue; _prevSlow = slowValue;
	}
}