趋势剥头皮策略(API/3858)
概览
TrendScalperStrategy 是 MetaTrader 4 专家顾问 Currencyprofits_01_1.mq4 的 StockSharp 移植版本。原始脚本是一套轻量的趋势型剥头皮系统:通过 EMA/SMA 交叉确认趋势,并利用最近几个高点和低点的突破来寻找入场机会。本移植版本保持原有交易逻辑,同时使用 StockSharp 的高级蜡烛订阅与指标管线实现。
交易逻辑
- 指标
- 快速 EMA(默认 6)基于收盘价。
- 慢速 SMA(默认 12)基于收盘价。
- 最近 N 根 K 线的最高价与最低价(默认窗口 6)。
- 入场条件
- 做多:价格触及最近的低点带(
Lowest Low),且快速 EMA 高于慢速 SMA。策略按照资金管理规则下达市价买单。 - 做空:价格触及最近的高点带(
Highest High),且快速 EMA 低于慢速 SMA。策略发送市价卖单。 - 持仓期间不会再开新仓,与 MQL 版本的“单笔持仓”行为保持一致。
- 做多:价格触及最近的低点带(
- 出场条件
- 多单平仓:当 K 线高点突破
Highest High时,立即市价卖出平仓。 - 空单平仓:当 K 线低点跌破
Lowest Low时,市价买入平仓。 - 若
StopLossPoints > 0,StartProtection会在开仓时附加平台托管的止损单。
- 多单平仓:当 K 线高点突破
资金管理
与原始 EA 相同,提供三个仓位管理模式:
| 模式 | 说明 | 移植版本的行为 |
|---|---|---|
0 |
固定手数(LotsIfNoMM)。 |
直接返回 FixedVolume。 |
<0 |
根据账户余额和风险因子计算的分数手。 | 计算 ceil(balance * risk / 10000) / 10,最大不超过 100 手。 |
>0 |
根据余额与风险因子的整数手。 | 采用同一公式,但结果向上取整为整数,最少 1 手、最多 100 手。 |
账户余额优先使用 Portfolio.CurrentValue,若不可用则退回到 BeginValue。若依旧无法获取,则使用固定手数以保证回测可运行。
风险控制
- 止损:
StopLossPoints以价格点(pip)表示,在OnStarted中乘以Security.PriceStep后传入StartProtection,由框架自动管理保护性订单。 - 单笔持仓:只有在
Position == 0时才允许开新仓,完全复刻原脚本的防重叠仓位逻辑。
参数
| 名称 | 默认值 | 说明 |
|---|---|---|
CandleType |
15 分钟 | 计算指标与产生信号所用的蜡烛序列。 |
FastLength |
6 | 快速 EMA 周期。 |
SlowLength |
12 | 慢速 SMA 周期。 |
BreakoutWindow |
6 | 统计最高/最低价时参考的蜡烛数量。 |
FixedVolume |
0.1 手 | 在关闭资金管理或缺少余额信息时使用的手数。 |
MoneyManagementMode |
0 | 选择固定、分数或整数手的模式。 |
MoneyManagementRisk |
40 | 余额驱动仓位大小的风险系数。 |
StopLossPoints |
50 | 止损距离(点数),会在启动时换算成绝对价格。 |
实现细节
- 通过
SubscribeCandles().Bind(...)串联指标,不需要自行维护历史价格缓存。 - 代码中的注释全部使用英文,以符合仓库规范。
- 未改动任何测试文件,本次提交仅包含策略移植与文档。
使用建议
- 根据原策略的定位选择合适的短周期(剥头皮)K 线。
- 确保标的证券的
PriceStep已正确设置,以便止损距离能从点数准确换算为价格。 - 调整
MoneyManagementRisk时需谨慎,值越大意味着通过ceil(balance * risk / 10000)公式计算出的仓位越大。
精确定义: 通道最高价与最低价取自信号K线之前的 BreakoutWindow 根已完成 K线,不包含当前K线。买入要求 EMA(FastLength) > SMA(SlowLength) 且 currentLow <= previousWindowLow;卖出要求反向趋势且 currentHigh >= previousWindowHigh。持有多头时 currentHigh > previousWindowHigh 平仓,空头时 currentLow < previousWindowLow 平仓。所有指标与通道需完成预热。当 StopLossPoints > 0 时使用 StartProtection 设置保护。
namespace StockSharp.Samples.Strategies;
using System;
using System.Collections.Generic;
using System.Linq;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
/// <summary>
/// Fast EMA versus slow SMA direction filtered by a sweep of the previous
/// candle-high/low band. Independent natural exits and a native stop protect fills.
/// </summary>
public class TrendScalperStrategy : Strategy
{
private readonly StrategyParam<int> _fastLength;
private readonly StrategyParam<int> _slowLength;
private readonly StrategyParam<int> _breakoutWindow;
private readonly StrategyParam<decimal> _fixedVolume;
private readonly StrategyParam<int> _moneyManagementMode;
private readonly StrategyParam<decimal> _moneyManagementRisk;
private readonly StrategyParam<decimal> _stopLossPoints;
private readonly StrategyParam<DataType> _candleType;
private readonly Queue<decimal> _fastSeed = new();
private readonly Queue<decimal> _slowCloses = new();
private readonly Queue<decimal> _highs = new();
private readonly Queue<decimal> _lows = new();
private decimal? _fastEma;
private Order _pendingOrder;
public int FastLength { get => _fastLength.Value; set => _fastLength.Value = value; }
public int SlowLength { get => _slowLength.Value; set => _slowLength.Value = value; }
public int BreakoutWindow { get => _breakoutWindow.Value; set => _breakoutWindow.Value = value; }
public decimal FixedVolume { get => _fixedVolume.Value; set => _fixedVolume.Value = value; }
public int MoneyManagementMode { get => _moneyManagementMode.Value; set => _moneyManagementMode.Value = value; }
public decimal MoneyManagementRisk { get => _moneyManagementRisk.Value; set => _moneyManagementRisk.Value = value; }
public decimal StopLossPoints { get => _stopLossPoints.Value; set => _stopLossPoints.Value = value; }
public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
public TrendScalperStrategy()
{
_fastLength = Param(nameof(FastLength), 6).SetGreaterThanZero();
_slowLength = Param(nameof(SlowLength), 12).SetGreaterThanZero();
_breakoutWindow = Param(nameof(BreakoutWindow), 6).SetGreaterThanZero();
_fixedVolume = Param(nameof(FixedVolume), 0.1m).SetGreaterThanZero();
_moneyManagementMode = Param(nameof(MoneyManagementMode), 0);
_moneyManagementRisk = Param(nameof(MoneyManagementRisk), 40m).SetNotNegative();
_stopLossPoints = Param(nameof(StopLossPoints), 50m).SetNotNegative();
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(15).TimeFrame());
}
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
=> [(Security, CandleType), (Security, DataType.Level1)];
protected override void OnReseted()
{
base.OnReseted();
ClearState();
}
private void ClearState()
{
_fastSeed.Clear();
_slowCloses.Clear();
_highs.Clear();
_lows.Clear();
_fastEma = null;
_pendingOrder = null;
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
ClearState();
var step = Security?.PriceStep ?? 0m;
if (step <= 0m)
throw new InvalidOperationException("Trend Scalper requires a positive PriceStep.");
if (StopLossPoints > 0m)
{
StartProtection(new Unit(), new Unit(StopLossPoints * step, UnitTypes.Absolute),
useMarketOrders: true, isLocalStop: true);
foreach (var field in new[] { Level1Fields.BestBidPrice, Level1Fields.BestAskPrice })
{
var quotes = new Subscription(DataType.Level1, Security);
quotes.MarketData.BuildField = field;
SubscribeLevel1(quotes).Bind(ObserveQuote).Start();
}
}
SubscribeCandles(CandleType).Bind(ProcessCandle).Start();
}
private void ObserveQuote(Level1ChangeMessage message)
{
// A Level1 stream is needed for native stop processing between candle closes.
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
var hasBand = _highs.Count == BreakoutWindow;
var bandHigh = hasBand ? _highs.Max() : 0m;
var bandLow = hasBand ? _lows.Min() : 0m;
_highs.Enqueue(candle.HighPrice);
_lows.Enqueue(candle.LowPrice);
while (_highs.Count > BreakoutWindow) _highs.Dequeue();
while (_lows.Count > BreakoutWindow) _lows.Dequeue();
var price = candle.ClosePrice;
if (_fastEma is null)
{
_fastSeed.Enqueue(price);
if (_fastSeed.Count == FastLength)
_fastEma = _fastSeed.Average();
}
else
_fastEma += (price - _fastEma.Value) * 2m / (FastLength + 1);
_slowCloses.Enqueue(price);
while (_slowCloses.Count > SlowLength)
_slowCloses.Dequeue();
if (!hasBand || _fastEma is null || _slowCloses.Count < SlowLength ||
_pendingOrder is not null && _pendingOrder.State is not (OrderStates.Done or OrderStates.Failed))
return;
if (Position > 0m && candle.HighPrice > bandHigh)
{
Submit(Sides.Sell, Position, "Trend scalper band exit");
return;
}
if (Position < 0m && candle.LowPrice < bandLow)
{
Submit(Sides.Buy, Math.Abs(Position), "Trend scalper band exit");
return;
}
if (Position != 0m)
return;
var slow = _slowCloses.Average();
if (_fastEma > slow && candle.LowPrice <= bandLow)
Enter(Sides.Buy);
else if (_fastEma < slow && candle.HighPrice >= bandHigh)
Enter(Sides.Sell);
}
private void Enter(Sides side)
{
var volume = CalculateOrderVolume();
if (volume > 0m)
Submit(side, volume, "Trend scalper entry");
}
private decimal CalculateOrderVolume()
{
var volume = FixedVolume;
if (MoneyManagementMode != 0)
{
var balance = Portfolio?.CurrentValue ?? Portfolio?.BeginValue ?? 0m;
if (balance > 0m)
{
var fractional = Math.Ceiling(balance * MoneyManagementRisk / 10000m) / 10m;
volume = MoneyManagementMode > 0 ? Math.Max(1m, Math.Ceiling(fractional)) : fractional;
volume = Math.Min(volume, 100m);
}
}
if (Security?.VolumeStep is decimal step && step > 0m)
volume = decimal.Floor(volume / step) * step;
if (Security?.MinVolume is decimal min && min > 0m)
volume = Math.Max(volume, min);
if (Security?.MaxVolume is decimal max && max > 0m)
volume = Math.Min(volume, max);
return volume;
}
private void Submit(Sides side, decimal volume, string comment)
{
_pendingOrder = new Order
{
Security = Security,
Portfolio = Portfolio,
Type = OrderTypes.Market,
Side = side,
Volume = volume,
Comment = comment,
};
RegisterOrder(_pendingOrder);
}
}
import clr
import math
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Strategies")
clr.AddReference("StockSharp.BusinessEntities")
from System import TimeSpan, Decimal
from StockSharp.Messages import (
DataType, CandleStates, Level1Fields, OrderTypes, OrderStates, Sides,
Unit, UnitTypes,
)
from StockSharp.BusinessEntities import Order, Subscription
from StockSharp.Algo.Strategies import Strategy
class trend_scalper_strategy(Strategy):
"""Fast close EMA, slow close SMA, and previous-bar high/low breakout bands."""
def __init__(self):
super(trend_scalper_strategy, self).__init__()
self._fast_length = self.Param("FastLength", 6).SetGreaterThanZero()
self._slow_length = self.Param("SlowLength", 12).SetGreaterThanZero()
self._breakout_window = self.Param("BreakoutWindow", 6).SetGreaterThanZero()
self._fixed_volume = self.Param("FixedVolume", 0.1).SetGreaterThanZero()
self._money_management_mode = self.Param("MoneyManagementMode", 0)
self._money_management_risk = self.Param("MoneyManagementRisk", 40.0).SetNotNegative()
self._stop_loss_points = self.Param("StopLossPoints", 50.0).SetNotNegative()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(15)))
self._clear_state()
@property
def candle_type(self):
return self._candle_type.Value
def _clear_state(self):
self._fast_seed = []
self._slow_closes = []
self._highs = []
self._lows = []
self._fast_ema = None
self._pending_order = None
def OnReseted(self):
super(trend_scalper_strategy, self).OnReseted()
self._clear_state()
def GetWorkingSecurities(self):
return [(self.Security, self.candle_type), (self.Security, DataType.Level1)]
def OnStarted2(self, time):
super(trend_scalper_strategy, self).OnStarted2(time)
self._clear_state()
step = float(self.Security.PriceStep or 0)
if step <= 0:
raise ValueError("Trend Scalper requires a positive PriceStep.")
if self._stop_loss_points.Value > 0:
self.StartProtection(
Unit(), Unit(Decimal(float(self._stop_loss_points.Value) * step), UnitTypes.Absolute),
useMarketOrders=True, isLocalStop=True)
for field in (Level1Fields.BestBidPrice, Level1Fields.BestAskPrice):
quotes = Subscription(DataType.Level1, self.Security)
quotes.MarketData.BuildField = field
self.SubscribeLevel1(quotes).Bind(self._observe_quote).Start()
self.SubscribeCandles(self.candle_type).Bind(self._process_candle).Start()
def _observe_quote(self, message):
# Keeps native protection informed between completed candles.
pass
def _process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
has_band = len(self._highs) == self._breakout_window.Value
band_high = max(self._highs) if has_band else 0.0
band_low = min(self._lows) if has_band else 0.0
self._highs.append(float(candle.HighPrice))
self._lows.append(float(candle.LowPrice))
if len(self._highs) > self._breakout_window.Value:
self._highs.pop(0)
self._lows.pop(0)
close = float(candle.ClosePrice)
fast_length = self._fast_length.Value
if self._fast_ema is None:
self._fast_seed.append(close)
if len(self._fast_seed) == fast_length:
self._fast_ema = sum(self._fast_seed) / fast_length
else:
self._fast_ema += (close - self._fast_ema) * 2.0 / (fast_length + 1)
self._slow_closes.append(close)
if len(self._slow_closes) > self._slow_length.Value:
self._slow_closes.pop(0)
if not has_band or self._fast_ema is None or len(self._slow_closes) < self._slow_length.Value:
return
if (self._pending_order is not None and
self._pending_order.State not in (OrderStates.Done, OrderStates.Failed)):
return
if self.Position > 0 and float(candle.HighPrice) > band_high:
self._submit(Sides.Sell, float(self.Position), "Trend scalper band exit")
return
if self.Position < 0 and float(candle.LowPrice) < band_low:
self._submit(Sides.Buy, abs(float(self.Position)), "Trend scalper band exit")
return
if self.Position != 0:
return
slow = sum(self._slow_closes) / len(self._slow_closes)
if self._fast_ema > slow and float(candle.LowPrice) <= band_low:
self._enter(Sides.Buy)
elif self._fast_ema < slow and float(candle.HighPrice) >= band_high:
self._enter(Sides.Sell)
def _enter(self, side):
volume = self._calculate_volume()
if volume > 0:
self._submit(side, volume, "Trend scalper entry")
def _calculate_volume(self):
volume = float(self._fixed_volume.Value)
mode = self._money_management_mode.Value
if mode != 0:
balance = (self.Portfolio.CurrentValue or self.Portfolio.BeginValue
if self.Portfolio is not None else 0)
balance = float(balance or 0)
if balance > 0:
fraction = math.ceil(balance * float(self._money_management_risk.Value) / 10000.0) / 10.0
volume = min(100.0, max(1.0, math.ceil(fraction)) if mode > 0 else fraction)
step = float(self.Security.VolumeStep or 0)
if step > 0:
volume = math.floor(volume / step) * step
minimum = float(self.Security.MinVolume or 0)
maximum = float(self.Security.MaxVolume or 0)
if minimum > 0:
volume = max(volume, minimum)
if maximum > 0:
volume = min(volume, maximum)
return volume
def _submit(self, side, volume, comment):
order = Order()
order.Security = self.Security
order.Portfolio = self.Portfolio
order.Type = OrderTypes.Market
order.Side = side
order.Volume = Decimal(volume)
order.Comment = comment
self._pending_order = order
self.RegisterOrder(order)
def CreateClone(self):
return trend_scalper_strategy()