在 GitHub 上查看
MACD Sample 1010 策略
概述
本模块将 MetaTrader 专家顾问 macd_sample_1010.mq4 迁移到 StockSharp 的高级 API。原始脚本在布林带的基础上叠加了简单的资金管理:当收盘价突破上轨并超过指定缓冲时开空单,跌破下轨并超过缓冲时开多单;随后在达到固定的盈利或亏损(以点数表示)时平仓。移植后的 StockSharp 策略订阅所选 K 线序列,绑定 BollingerBands 指标,并在回调中发送市价单与管理仓位,以完整复现上述流程。
该移植版本同样只在收盘后做出判断,确保突破与出场的判定与 MetaTrader 中基于收盘价的逻辑一致。同时保留了原脚本的 LotIncrease 动态加仓选项,可根据账户权益变化自动调整交易手数。
移植要点
- 采用
SubscribeCandles + Bind 的高级模式为 BollingerBands 指标提供数据,无需手动缓存或循环计算。
- 使用
StrategyParam<T> 声明所有输入参数,使其在界面中可视并可用于优化。
- 通过读取
Security.PriceStep 将点值换算为价格增量,从而在 BuyMarket / SellMarket 调用前加上与 MetaTrader 相同的缓冲距离。
- 利用
Portfolio.CurrentValue(若不可用则回退至 BeginValue)实现与 MQL4 版本一致的动态手数计算,且将结果限制在 500 手以内。
- 仅处理已完成的 K 线,避免原脚本通过计数器抑制的逐笔抖动。
- 在关键逻辑处加入英文注释,帮助理解各个处理步骤。
参数
| 参数 |
类型 |
默认值 |
说明 |
ProfitTargetPips |
decimal |
3 |
单笔多空在获利 ProfitTargetPips 个点后平仓,设置为 0 则关闭止盈。 |
LossLimitPips |
decimal |
20 |
单笔多空在亏损 LossLimitPips 个点后平仓,设置为 0 则关闭止损。 |
BandDistancePips |
decimal |
3 |
在上轨之上/下轨之下额外增加的点数缓冲,用于确认突破。 |
BollingerPeriod |
int |
4 |
计算布林带的周期长度。 |
BollingerDeviation |
decimal |
2 |
布林带使用的标准差倍数。 |
BaseVolume |
decimal |
1 |
初始下单手数,同时也是动态手数算法的基准。 |
LotIncrease |
bool |
true |
启用后,按照当前账户权益与初始权益的比例调整交易手数。 |
OneOrderOnly |
bool |
true |
为 true 时只有在无持仓时才会再次开仓;即便关闭,由于 StockSharp 使用净头寸,也不会出现对冲持仓。 |
CandleType |
DataType |
TimeFrame(15m) |
指标计算与信号判断使用的 K 线类型。 |
交易逻辑
OnStarted 中根据参数创建布林带指标,订阅 CandleType 所指的 K 线,并绑定 ProcessCandle 回调。
- 每根完成的 K 线触发
ProcessCandle,若启用了 LotIncrease 会先更新当前下单手数。
- 若收盘价高于上轨加上
BandDistancePips(换算成价格),策略发送市价卖单;若收盘价低于下轨减去缓冲,则发送市价买单。OneOrderOnly 为真时,仅在净头寸为零时允许新开仓。
- 执行完可能的入场后检查当前头寸:
- 多单在盈利达到
ProfitTargetPips 点或亏损达到 LossLimitPips 点时平仓。
- 空单在盈利达到
ProfitTargetPips 点或亏损达到 LossLimitPips 点时平仓。
- 所有点数比较均通过
Security.PriceStep 转换为价格增量,确保与 MQL4 版本的 pip 计算保持一致。
手数控制
- 关闭
LotIncrease 时始终按 BaseVolume 下单。
- 启用
LotIncrease 时,策略在首次计算时记录“每手对应的初始权益”,之后每根 K 线依据当前权益与该基准的比值重新计算手数,四舍五入到一位小数(等同于 MQL4 的 NormalizeDouble(..., 1)),并限制在 500 手以内。
- 当账户权益信息不可用时,会回退到固定手数模式。
使用建议
- 在所选品种上启动策略前,确认
Security.PriceStep 与实际交易的最小价格变动相符。
- 设置合适的
CandleType 时间框架。原脚本常用于 5–15 分钟级别,但也可使用其他周期。
- 根据需求调整布林带参数、缓冲点数以及止盈止损阈值。
- 决定是否启用余额驱动的手数放大功能 (
LotIncrease)。
- 启动策略并观察日志,确认入场与出场发生在预期的收盘价附近。
- StockSharp 只维护净头寸,因此即便关闭
OneOrderOnly 也不会出现多空同时持仓的情形,而是通过增减净头寸实现加仓或反手。
- 止盈止损通过每根收盘 K 线检查来实现,而非在服务器上挂出预设的限价单,但行为与原脚本等效。
- 原脚本的日志与错误开关(
logging、logerrs、logtick)在移植过程中省略,借助 StockSharp 自带的日志系统即可跟踪所有订单与成交。
- MetaTrader 版本在本地文件中记录统计数据,本移植依托 StockSharp 的组合与策略统计功能,不再生成额外文件。
using System;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// MACD Sample 1010: SMA band mean reversion with ATR stops.
/// </summary>
public class MacdSample1010Strategy : Strategy
{
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _smaLength;
private readonly StrategyParam<int> _atrLength;
private readonly StrategyParam<decimal> _bandMult;
private decimal _entryPrice;
public MacdSample1010Strategy()
{
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Timeframe.", "General");
_smaLength = Param(nameof(SmaLength), 20)
.SetDisplay("SMA Length", "Band center period.", "Indicators");
_atrLength = Param(nameof(AtrLength), 14)
.SetDisplay("ATR Length", "ATR period.", "Indicators");
_bandMult = Param(nameof(BandMult), 2.0m)
.SetDisplay("Band Multiplier", "ATR multiplier for bands.", "Indicators");
}
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
public int SmaLength
{
get => _smaLength.Value;
set => _smaLength.Value = value;
}
public int AtrLength
{
get => _atrLength.Value;
set => _atrLength.Value = value;
}
public decimal BandMult
{
get => _bandMult.Value;
set => _bandMult.Value = value;
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_entryPrice = 0;
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_entryPrice = 0;
var sma = new SimpleMovingAverage { Length = SmaLength };
var atr = new AverageTrueRange { Length = AtrLength };
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(sma, atr, ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, sma);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle, decimal smaVal, decimal atrVal)
{
if (candle.State != CandleStates.Finished)
return;
if (atrVal <= 0)
return;
var close = candle.ClosePrice;
var upper = smaVal + atrVal * BandMult;
var lower = smaVal - atrVal * BandMult;
if (Position > 0)
{
if (close >= smaVal || close <= _entryPrice - atrVal * 2m)
{
SellMarket();
_entryPrice = 0;
}
}
else if (Position < 0)
{
if (close <= smaVal || close >= _entryPrice + atrVal * 2m)
{
BuyMarket();
_entryPrice = 0;
}
}
if (Position == 0)
{
if (close < lower)
{
_entryPrice = close;
BuyMarket();
}
else if (close > upper)
{
_entryPrice = close;
SellMarket();
}
}
}
}
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 SimpleMovingAverage, AverageTrueRange
from StockSharp.Algo.Strategies import Strategy
class macd_sample_1010_strategy(Strategy):
"""
SMA band mean reversion with ATR-based bands and stops.
"""
def __init__(self):
super(macd_sample_1010_strategy, self).__init__()
self._sma_length = self.Param("SmaLength", 20).SetDisplay("SMA Length", "Band center period", "Indicators")
self._atr_length = self.Param("AtrLength", 14).SetDisplay("ATR Length", "ATR period", "Indicators")
self._band_mult = self.Param("BandMult", 2.0).SetDisplay("Band Multiplier", "ATR multiplier for bands", "Indicators")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))).SetDisplay("Candle Type", "Candles", "General")
self._entry_price = 0.0
@property
def candle_type(self):
return self._candle_type.Value
def OnReseted(self):
super(macd_sample_1010_strategy, self).OnReseted()
self._entry_price = 0.0
def OnStarted2(self, time):
super(macd_sample_1010_strategy, self).OnStarted2(time)
sma = SimpleMovingAverage()
sma.Length = self._sma_length.Value
atr = AverageTrueRange()
atr.Length = self._atr_length.Value
subscription = self.SubscribeCandles(self.candle_type)
subscription.Bind(sma, atr, self._process_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, sma)
self.DrawOwnTrades(area)
def _process_candle(self, candle, sma_val, atr_val):
if candle.State != CandleStates.Finished:
return
sma = float(sma_val)
atr = float(atr_val)
if atr <= 0:
return
close = float(candle.ClosePrice)
bm = float(self._band_mult.Value)
upper = sma + atr * bm
lower = sma - atr * bm
if self.Position > 0:
if close >= sma or close <= self._entry_price - atr * 2.0:
self.SellMarket()
self._entry_price = 0.0
elif self.Position < 0:
if close <= sma or close >= self._entry_price + atr * 2.0:
self.BuyMarket()
self._entry_price = 0.0
if self.Position == 0:
if close < lower:
self._entry_price = close
self.BuyMarket()
elif close > upper:
self._entry_price = close
self.SellMarket()
def CreateClone(self):
return macd_sample_1010_strategy()