AK-47 剥头皮策略
本策略基于 MetaTrader 5 专家顾问 “AK-47 Scalper EA”(版本 44883) 重写,实现于 StockSharp 的高级策略框架中。
算法会在允许的交易时间段内始终保持一个卖出止损挂单。挂单被触发后会立刻附加止损和止盈保护订单。随着市场波动,挂单价格以及保护性止损都会被动态收紧。
核心流程
- 根据品种的最小跳动值计算点值(对于 5 位小数品种,点值会放大 10 倍以匹配 MT5 行为)。
- 评估交易时间窗口,启用时间过滤时只有在起止时间之间才允许开仓,窗口可以跨越午夜。
- 检查当前点差是否超过阈值,点差过大时不会下单。
- 计算下单手数:
- 可以直接使用固定手数(
Base Lot参数),或 - 使用账户权益的
Risk Percent百分比换算出手数,并按照交易所的最小/最大/步长规则对齐。
- 可以直接使用固定手数(
- 在买价下方
SL/2点处放置卖出止损订单,同时预先计算好在买价上方SL/2点的止损价和低于入场TP点的止盈价。 - 挂单等待期间持续使用
ReRegisterOrder调整价格,使其始终与买价保持SL/2点的间距,并更新计划中的保护价格。 - 挂单成交后:
- 依据计划价格注册买入止损(止损)和买入限价(止盈)订单。
- 每根 K 线收盘时,将止损保持在买价上方
SL点的位置,如果价格继续向盈利方向移动会同步下移。 - 止盈价保持不变。
- 仓位清空时撤销所有保护订单,等待新的交易机会。
参数说明
| 参数 | 含义 |
|---|---|
| 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();
}
}
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 AverageTrueRange
from StockSharp.Algo.Strategies import Strategy
class ak47_scalper_strategy(Strategy):
def __init__(self):
super(ak47_scalper_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5)))
self._lookback_period = self.Param("LookbackPeriod", 5)
self._atr_period = self.Param("AtrPeriod", 14)
self._atr_stop_multiplier = self.Param("AtrStopMultiplier", 1.5)
self._highest_high = 0.0
self._lowest_low = float('inf')
self._bars_collected = 0
self._entry_price = None
self._entry_side = None
self._stop_distance = 0.0
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
@property
def LookbackPeriod(self):
return self._lookback_period.Value
@LookbackPeriod.setter
def LookbackPeriod(self, value):
self._lookback_period.Value = value
@property
def AtrPeriod(self):
return self._atr_period.Value
@AtrPeriod.setter
def AtrPeriod(self, value):
self._atr_period.Value = value
@property
def AtrStopMultiplier(self):
return self._atr_stop_multiplier.Value
@AtrStopMultiplier.setter
def AtrStopMultiplier(self, value):
self._atr_stop_multiplier.Value = value
def OnReseted(self):
super(ak47_scalper_strategy, self).OnReseted()
self._highest_high = 0.0
self._lowest_low = float('inf')
self._bars_collected = 0
self._entry_price = None
self._entry_side = None
self._stop_distance = 0.0
def OnStarted2(self, time):
super(ak47_scalper_strategy, self).OnStarted2(time)
self._highest_high = 0.0
self._lowest_low = float('inf')
self._bars_collected = 0
self._entry_price = None
self._entry_side = None
self._stop_distance = 0.0
atr = AverageTrueRange()
atr.Length = self.AtrPeriod
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(atr, self._process_candle).Start()
def _process_candle(self, candle, atr_value):
if candle.State != CandleStates.Finished:
return
atr_val = float(atr_value)
high = float(candle.HighPrice)
low = float(candle.LowPrice)
close = float(candle.ClosePrice)
lookback = self.LookbackPeriod
# Build lookback channel
if self._bars_collected < lookback:
if high > self._highest_high:
self._highest_high = high
if low < self._lowest_low:
self._lowest_low = low
self._bars_collected += 1
return
atr_stop_mult = float(self.AtrStopMultiplier)
self._stop_distance = atr_val * atr_stop_mult
# Check stop loss / take profit on existing position
if self._entry_price is not None and self._entry_side is not None:
if self._entry_side == "buy" and close <= self._entry_price - self._stop_distance:
self.SellMarket()
self._entry_price = None
self._entry_side = None
elif self._entry_side == "sell" and close >= self._entry_price + self._stop_distance:
self.BuyMarket()
self._entry_price = None
self._entry_side = None
elif self._entry_side == "buy" and close >= self._entry_price + self._stop_distance * 1.5:
self.SellMarket()
self._entry_price = None
self._entry_side = None
elif self._entry_side == "sell" and close <= self._entry_price - self._stop_distance * 1.5:
self.BuyMarket()
self._entry_price = None
self._entry_side = None
# Entry signals: breakout
if self.Position == 0:
if close > self._highest_high:
self.BuyMarket()
self._entry_price = close
self._entry_side = "buy"
elif close < self._lowest_low:
self.SellMarket()
self._entry_price = close
self._entry_side = "sell"
# Update channel with slow decay
if high > self._highest_high:
self._highest_high = high
else:
self._highest_high = self._highest_high * 0.999 + high * 0.001
if low < self._lowest_low:
self._lowest_low = low
else:
self._lowest_low = self._lowest_low * 0.999 + low * 0.001
def CreateClone(self):
return ak47_scalper_strategy()