在 GitHub 上查看
概述
MetaTrader 4 的 TraderToolEA v1.8 并不是自动化机器人,而是一套帮助交易者下单和管理仓位的操作面板。
在 StockSharp 中我们将其转换为策略参数:每个布尔参数就像原面板上的按钮,设置为 true 即触发对应动作。
核心功能如下:
- 一键买入/卖出,用于快速开仓或平仓。
- 根据最新报价构建对称的止损单或限价单网格。
- 带有“孤儿单”清理机制的挂单撤销指令(按买/卖或全部撤销)。
- 基于 Level1 数据的虚拟止损、止盈、追踪止损以及保本保护。
Use Auto Volume + Risk Factor 组合复制了 MT4 版本的自动手数计算方式。
实现完全基于高层 API:订阅 DataType.Level1、调用 BuyStop/SellLimit 等封装方法以及默认的日志系统。
参数说明
| 名称 |
说明 |
Use Auto Volume |
设为 true 时按组合市值和 Risk Factor 计算手数;否则使用固定的 Order Volume。 |
Risk Factor |
自动手数的风险系数,对应 MT4 输入参数 RiskFactor。 |
Order Volume |
手动下单时的固定手数。 |
Distance (pips) |
网格间距(以 MetaTrader 的 pip 为单位),适用于止损单和限价单。 |
Layers |
每次命令生成的额外挂单数量,模拟原版多次点击按钮的效果。 |
Delete Orphans |
开启后,当网格失衡时会自动撤销多余的一侧挂单,保持买卖数量对等。 |
Enable Stop Loss / Stop Loss (pips) |
开启/设置固定止损距离(以 pip 表示)。 |
Enable Take Profit / Take Profit (pips) |
开启/设置固定止盈距离。 |
Enable Trailing / Trailing (pips) |
启用追踪止损,当浮盈超过设定值后才开始移动。 |
Enable Break-Even / Break-Even Trigger / Break-Even Lock |
达到触发距离后,将止损移动到入场价并加上锁定点差。 |
控制开关(Open Buy, Place Buy Stops, Delete Sell Limits 等) |
对应原面板的按钮。设为 true 即执行,完成后自动复位为 false。 |
工作流程
- 数据源:仅订阅 Level1,所有价格更新均来自最新的买/卖报价。
- 手数归一化:提交订单前,手数按照
VolumeStep 对齐,并限制在 MinVolume 与 MaxVolume 之间。
- 挂单布网:以最近的 bid/ask 为基准生成价位,并对齐到价格最小变动 (
PriceStep)。
- 孤儿单清理:当
Delete Orphans 为 true 时,若买卖挂单数量不一致,将撤销多余的一侧(止损单和限价单分别处理)。
- 虚拟保护:止损、止盈、追踪和保本均以虚拟方式实现——价格触发时直接发送市价单平仓,并重置内部状态。
与原版的差异
- 所有界面元素(按钮、颜色、声音等)被参数和日志替代,可在 StockSharp 界面或脚本中操作。
- 保护逻辑通过平仓市价单完成,而非修改订单的止损/止盈价格,确保在不同券商下行为一致。
- MT4 的
ManageOrders 模式合并为“仅管理本策略的订单”。
- 自动手数使用组合估值代替
AccountBalance(),但计算公式保持一致。
使用建议
- 在交易连接中设置好
PriceStep、VolumeStep、MinVolume、LotSize 等属性,确保 pip 转换与手数归一化准确。
- 可在界面上绑定快捷键或按钮到这些布尔参数,重现原面板的操作体验。
- 需要对称网格时建议启用
Delete Orphans,避免出现孤立挂单。
- 所有被跳过的操作都会在日志中给出原因(例如缺少报价或手数为零)。
- 因为保护逻辑是虚拟的,只要有仓位存在就应保持策略运行,以便及时发送平仓单。
移植细节
- pip 大小遵循 MetaTrader 规则:3/5 位小数的品种在
PriceStep 基础上乘以 10。
- 追踪止损和保本逻辑完全对应 MQL 实现:只有在浮盈出现时才会激活,并在成交/反向后重置。
- 原面板支持多次点击来扩展网格,
Layers 参数通过一次命令直接生成多层。
- 所有控制开关都设置了
SetCanOptimize(false),防止在优化过程中意外触发。
using System;
using Ecng.Common;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Trader Tool Manager strategy: EMA + Momentum trend follower.
/// Buys when close crosses above EMA and momentum > 100.
/// Sells when close crosses below EMA and momentum < 100.
/// </summary>
public class TraderToolEaStrategy : Strategy
{
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _emaPeriod;
private readonly StrategyParam<int> _momPeriod;
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
public int EmaPeriod
{
get => _emaPeriod.Value;
set => _emaPeriod.Value = value;
}
public int MomPeriod
{
get => _momPeriod.Value;
set => _momPeriod.Value = value;
}
public TraderToolEaStrategy()
{
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
.SetDisplay("Candle Type", "Candle timeframe", "General");
_emaPeriod = Param(nameof(EmaPeriod), 30)
.SetGreaterThanZero()
.SetDisplay("EMA Period", "EMA period", "Indicators");
_momPeriod = Param(nameof(MomPeriod), 20)
.SetGreaterThanZero()
.SetDisplay("Momentum", "Momentum period", "Indicators");
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
var ema = new ExponentialMovingAverage { Length = EmaPeriod };
var mom = new Momentum { Length = MomPeriod };
decimal? prevClose = null;
decimal? prevEma = null;
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(ema, mom, (candle, emaVal, momVal) =>
{
if (candle.State != CandleStates.Finished)
return;
if (!IsFormedAndOnlineAndAllowTrading())
return;
var close = candle.ClosePrice;
if (prevClose.HasValue && prevEma.HasValue)
{
var crossUp = prevClose.Value <= prevEma.Value && close > emaVal;
var crossDown = prevClose.Value >= prevEma.Value && close < emaVal;
if (crossUp && momVal > 100m && Position <= 0)
BuyMarket();
else if (crossDown && momVal < 100m && Position >= 0)
SellMarket();
}
prevClose = close;
prevEma = emaVal;
})
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, ema);
DrawOwnTrades(area);
}
}
}
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 ExponentialMovingAverage, Momentum
from StockSharp.Algo.Strategies import Strategy
class trader_tool_ea_strategy(Strategy):
def __init__(self):
super(trader_tool_ea_strategy, self).__init__()
self._ema_period = self.Param("EmaPeriod", 30) \
.SetDisplay("EMA Period", "EMA period", "Indicators")
self._mom_period = self.Param("MomPeriod", 20) \
.SetDisplay("Momentum", "Momentum period", "Indicators")
self._ema = None
self._mom = None
self._prev_close = None
self._prev_ema = None
@property
def ema_period(self):
return self._ema_period.Value
@property
def mom_period(self):
return self._mom_period.Value
def OnReseted(self):
super(trader_tool_ea_strategy, self).OnReseted()
self._ema = None
self._mom = None
self._prev_close = None
self._prev_ema = None
def OnStarted2(self, time):
super(trader_tool_ea_strategy, self).OnStarted2(time)
self._ema = ExponentialMovingAverage()
self._ema.Length = self.ema_period
self._mom = Momentum()
self._mom.Length = self.mom_period
subscription = self.SubscribeCandles(DataType.TimeFrame(TimeSpan.FromHours(1)))
subscription.Bind(self._ema, self._mom, self._process_candle)
subscription.Start()
def _process_candle(self, candle, ema_value, mom_value):
if candle.State != CandleStates.Finished:
return
if not self._ema.IsFormed or not self._mom.IsFormed:
return
close = float(candle.ClosePrice)
ema_val = float(ema_value)
mom_val = float(mom_value)
if self._prev_close is not None and self._prev_ema is not None:
cross_up = self._prev_close <= self._prev_ema and close > ema_val
cross_down = self._prev_close >= self._prev_ema and close < ema_val
if cross_up and mom_val > 100.0 and self.Position <= 0:
self.BuyMarket()
elif cross_down and mom_val < 100.0 and self.Position >= 0:
self.SellMarket()
self._prev_close = close
self._prev_ema = ema_val
def CreateClone(self):
return trader_tool_ea_strategy()