在 GitHub 上查看

AK-47 剥头皮策略

本策略基于 MetaTrader 5 专家顾问 “AK-47 Scalper EA”(版本 44883) 重写,实现于 StockSharp 的高级策略框架中。

算法会在允许的交易时间段内始终保持一个卖出止损挂单。挂单被触发后会立刻附加止损和止盈保护订单。随着市场波动,挂单价格以及保护性止损都会被动态收紧。

核心流程

  1. 根据品种的最小跳动值计算点值(对于 5 位小数品种,点值会放大 10 倍以匹配 MT5 行为)。
  2. 评估交易时间窗口,启用时间过滤时只有在起止时间之间才允许开仓,窗口可以跨越午夜。
  3. 检查当前点差是否超过阈值,点差过大时不会下单。
  4. 计算下单手数:
    • 可以直接使用固定手数(Base Lot 参数),或
    • 使用账户权益的 Risk Percent 百分比换算出手数,并按照交易所的最小/最大/步长规则对齐。
  5. 在买价下方 SL/2 点处放置卖出止损订单,同时预先计算好在买价上方 SL/2 点的止损价和低于入场 TP 点的止盈价。
  6. 挂单等待期间持续使用 ReRegisterOrder 调整价格,使其始终与买价保持 SL/2 点的间距,并更新计划中的保护价格。
  7. 挂单成交后:
    • 依据计划价格注册买入止损(止损)和买入限价(止盈)订单。
    • 每根 K 线收盘时,将止损保持在买价上方 SL 点的位置,如果价格继续向盈利方向移动会同步下移。
    • 止盈价保持不变。
  8. 仓位清空时撤销所有保护订单,等待新的交易机会。

参数说明

参数 含义
Use Risk Percent 切换为使用权益百分比计算手数。
Risk Percent 以权益百分比计算手数时使用的比例。
Base Lot 固定手数,同时也是风险模式的对齐步长。
Stop Loss (pips) 止损距离,挂单价格会使用其中一半的距离。
Take Profit (pips) 止盈距离,设为 0 可关闭止盈。
Max Spread (points) 允许的最大点差(以 MT5 点表示)。
Use Time Filter 是否启用交易时间过滤。
Start Hour / Minute 交易窗口起始时间。
End Hour / Minute 交易窗口结束时间。
Candle Type 用于驱动策略逻辑的 K 线数据类型。

其他说明

  • 策略只会进行卖出方向的交易,与原始 EA 相同。
  • 为了兼容 StockSharp 高级 API,所有的跟踪止损都在 K 线收盘时执行。
  • 保护订单通过 ReRegisterOrder 进行改价,请确认目标撮合环境支持订单改价功能。
  • 原 EA 中的终端注释未迁移,策略改为依赖日志输出。
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 "AK-47 Scalper" MetaTrader expert.
/// Sells when price breaks below the low of the previous N candles (breakout scalp),
/// buys when price breaks above the high. Uses ATR for stop distance management.
/// </summary>
public class Ak47ScalperStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _lookbackPeriod;
	private readonly StrategyParam<int> _atrPeriod;
	private readonly StrategyParam<decimal> _atrStopMultiplier;

	private AverageTrueRange _atr;
	private decimal _highestHigh;
	private decimal _lowestLow;
	private int _barsCollected;
	private decimal? _entryPrice;
	private Sides? _entrySide;
	private decimal _stopDistance;

	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	public int LookbackPeriod
	{
		get => _lookbackPeriod.Value;
		set => _lookbackPeriod.Value = value;
	}

	public int AtrPeriod
	{
		get => _atrPeriod.Value;
		set => _atrPeriod.Value = value;
	}

	public decimal AtrStopMultiplier
	{
		get => _atrStopMultiplier.Value;
		set => _atrStopMultiplier.Value = value;
	}

	public Ak47ScalperStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle Type", "Timeframe", "General");

		_lookbackPeriod = Param(nameof(LookbackPeriod), 5)
			.SetGreaterThanZero()
			.SetDisplay("Lookback", "Number of bars for high/low channel", "Indicators");

		_atrPeriod = Param(nameof(AtrPeriod), 14)
			.SetGreaterThanZero()
			.SetDisplay("ATR Period", "ATR period for stop distance", "Indicators");

		_atrStopMultiplier = Param(nameof(AtrStopMultiplier), 1.5m)
			.SetGreaterThanZero()
			.SetDisplay("ATR Stop Mult", "ATR multiplier for stop distance", "Risk");
	}

	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		_atr = new AverageTrueRange { Length = AtrPeriod };
		_highestHigh = 0;
		_lowestLow = decimal.MaxValue;
		_barsCollected = 0;
		_entryPrice = null;
		_entrySide = null;
		_stopDistance = 0;

		var subscription = SubscribeCandles(CandleType);
		subscription
			.BindEx(_atr, ProcessCandle)
			.Start();

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

	private void ProcessCandle(ICandleMessage candle, IIndicatorValue atrValue)
	{
		if (candle.State != CandleStates.Finished)
			return;

		if (!atrValue.IsFinal)
			return;

		var atrDecimal = atrValue.IsEmpty ? 0m : atrValue.GetValue<decimal>();

		// Build lookback channel
		if (_barsCollected < LookbackPeriod)
		{
			if (candle.HighPrice > _highestHigh)
				_highestHigh = candle.HighPrice;
			if (candle.LowPrice < _lowestLow)
				_lowestLow = candle.LowPrice;
			_barsCollected++;
			return;
		}

		if (!_atr.IsFormed)
		{
			// Keep updating channel
			UpdateChannel(candle);
			return;
		}

		var close = candle.ClosePrice;
		var volume = Volume;
		if (volume <= 0)
			volume = 1;

		_stopDistance = atrDecimal * AtrStopMultiplier;

		// Check stop loss on existing position
		if (_entryPrice != null && _entrySide != null)
		{
			if (_entrySide == Sides.Buy && close <= _entryPrice.Value - _stopDistance)
			{
				SellMarket(Math.Abs(Position));
				_entryPrice = null;
				_entrySide = null;
			}
			else if (_entrySide == Sides.Sell && close >= _entryPrice.Value + _stopDistance)
			{
				BuyMarket(Math.Abs(Position));
				_entryPrice = null;
				_entrySide = null;
			}
			// Take profit at 2x ATR
			else if (_entrySide == Sides.Buy && close >= _entryPrice.Value + _stopDistance * 1.5m)
			{
				SellMarket(Math.Abs(Position));
				_entryPrice = null;
				_entrySide = null;
			}
			else if (_entrySide == Sides.Sell && close <= _entryPrice.Value - _stopDistance * 1.5m)
			{
				BuyMarket(Math.Abs(Position));
				_entryPrice = null;
				_entrySide = null;
			}
		}

		// Entry signals: breakout
		if (Position == 0)
		{
			if (close > _highestHigh)
			{
				BuyMarket(volume);
				_entryPrice = close;
				_entrySide = Sides.Buy;
			}
			else if (close < _lowestLow)
			{
				SellMarket(volume);
				_entryPrice = close;
				_entrySide = Sides.Sell;
			}
		}

		UpdateChannel(candle);
	}

	private void UpdateChannel(ICandleMessage candle)
	{
		// Simple rolling update - reset and let it rebuild
		// For simplicity, just use last candle's high/low as reference shifted
		if (candle.HighPrice > _highestHigh)
			_highestHigh = candle.HighPrice;
		else
			_highestHigh = _highestHigh * 0.999m + candle.HighPrice * 0.001m; // slow decay

		if (candle.LowPrice < _lowestLow)
			_lowestLow = candle.LowPrice;
		else
			_lowestLow = _lowestLow * 0.999m + candle.LowPrice * 0.001m; // slow decay
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		_atr = null;
		_highestHigh = 0;
		_lowestLow = decimal.MaxValue;
		_barsCollected = 0;
		_entryPrice = null;
		_entrySide = null;
		_stopDistance = 0;

		base.OnReseted();
	}
}