在 GitHub 上查看
回测交易助手面板策略
概述
Backtesting Trade Assistant Panel Strategy 源自 MetaTrader 4 专家顾问 Backtesting Trade Assistant Panel V1.10,原版在测试器窗口中绘制按钮与输入框,允许交易者在回测阶段手动调整手数、止损、止盈,并立即发送 BUY/SELL 市价单。迁移到 StockSharp 后,图形界面被策略参数与公开方法替代,但功能完全等价,依旧侧重于“人工下单 + 自动挂保护单”的流程。
主要特性:
- 以参数形式维护下单手数、止损与止盈距离(单位为 MetaTrader “point”)。
- 通过
ManualBuy()、ManualSell() 方法随时触发多/空市价单。
- 在每次下单后自动根据点数距离调用
SetStopLoss、SetTakeProfit 添加保护性委托。
- 提供
SetOrderVolume、SetStopLoss、SetTakeProfit 等工具函数,对应 MT4 面板上的可编辑文本框,运行中亦可调整。
参数
| 名称 |
说明 |
默认值 |
OrderVolume |
市价下单时使用的手数,同时同步到基础的 Strategy.Volume。 |
0.1 |
StopLossPips |
止损距离(以 point 为单位)。设为 0 时不自动挂出止损单。 |
50 |
TakeProfitPips |
止盈距离(以 point 为单位)。设为 0 时不自动挂出止盈单。 |
100 |
MagicNumber |
保留自原始 EA 的标识符,方便扩展或日志使用,StockSharp 本身不会读取。 |
99 |
手动操作
原版靠按钮触发动作,StockSharp 版本改为公开方法:
SetOrderVolume(decimal volume) —— 同步下单手数并写入 Strategy.Volume。
SetStopLoss(decimal pips) / SetTakeProfit(decimal pips) —— 动态调整止损/止盈点数,和 MT4 文本框的含义一致。
ManualBuy() —— 按当前手数发送买入市价单,并基于合约信息将点数距离换算成价格差,随后调用 SetStopLoss、SetTakeProfit。
ManualSell() —— 发送卖出市价单,逻辑与 ManualBuy() 对称。
CloseAllPositions() —— 立即平掉所有持仓,对应测试中手动“flatten”的需求。
换算点值时沿用 MT4 的惯例:对于报价保留 5 位或 3 位小数的品种,PriceStep 会乘以 10 视为一个 point;其他品种直接使用 PriceStep。若行情缺失相关元数据,则退化为 0.0001,确保行为一致。
行为说明
- 策略订阅 Level1 行情以获取最新买卖价,若不可用则退回到最近成交价,再去挂保护单。
- 本策略不生成自动化信号,定位仍是“人工辅助执行器”。
MagicNumber 仅为兼容字段,若需要进一步分类或记录,可在自定义扩展中引用。
- 在调用
ManualBuy()/ManualSell() 之前可以随时修改止损、止盈与手数,完全模拟原始面板的交互流程。
与原 EA 的差异
- 图形界面被参数和方法取代,所有功能通过程序调用即可完成。
- MT4
OrderSend 中固定 50 point 的滑点限制未迁移,StockSharp 的 BuyMarket/SellMarket 不提供对应参数,必要时请在外部风控或撮合层处理。
- 保护单通过 StockSharp 的高层 API (
SetStopLoss/SetTakeProfit) 生成,更符合框架约定。
使用建议
- 在 StockSharp 中配置好交易品种、投资组合及连接后启动策略。
- 通过参数面板或方法调整
OrderVolume、StopLossPips、TakeProfitPips。
- 需要进场时调用
ManualBuy() 或 ManualSell(),策略会自动挂出相应的保护单。
- 使用
CloseAllPositions() 可以在回测或实时演练中快速平仓。
using System;
using System.Linq;
using System.Collections.Generic;
using Ecng.Common;
using Ecng.Collections;
using Ecng.Serialization;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Trade assistant strategy with configurable stop-loss and take-profit.
/// Simplified from the backtesting trade assistant panel.
/// </summary>
public class BacktestingTradeAssistantPanelStrategy : Strategy
{
private readonly StrategyParam<decimal> _stopLossPips;
private readonly StrategyParam<decimal> _takeProfitPips;
private readonly StrategyParam<DataType> _candleType;
private SimpleMovingAverage _sma;
private decimal _pipSize;
private decimal _entryPrice;
private decimal? _stopPrice;
private decimal? _takePrice;
/// <summary>
/// Stop loss distance in pips.
/// </summary>
public decimal StopLossPips
{
get => _stopLossPips.Value;
set => _stopLossPips.Value = value;
}
/// <summary>
/// Take profit distance in pips.
/// </summary>
public decimal TakeProfitPips
{
get => _takeProfitPips.Value;
set => _takeProfitPips.Value = value;
}
/// <summary>
/// Candle type for signals.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initializes strategy parameters.
/// </summary>
public BacktestingTradeAssistantPanelStrategy()
{
_stopLossPips = Param(nameof(StopLossPips), 50m)
.SetNotNegative()
.SetDisplay("Stop Loss (pips)", "Stop-loss distance in pips", "Risk");
_takeProfitPips = Param(nameof(TakeProfitPips), 100m)
.SetNotNegative()
.SetDisplay("Take Profit (pips)", "Take-profit distance in pips", "Risk");
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
.SetDisplay("Candle Type", "Candle series for trading signals", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
yield return (Security, CandleType);
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_sma = null;
_pipSize = 0m;
_entryPrice = 0m;
_stopPrice = null;
_takePrice = null;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_pipSize = CalculatePipSize();
_sma = new SimpleMovingAverage { Length = 20 };
SubscribeCandles(CandleType)
.Bind(_sma, ProcessCandle)
.Start();
}
private void ProcessCandle(ICandleMessage candle, decimal smaValue)
{
if (candle.State != CandleStates.Finished)
return;
if (!IsFormed)
return;
var price = candle.ClosePrice;
// Check stop-loss and take-profit
if (Position != 0 && _entryPrice > 0m)
{
if (Position > 0)
{
if (_stopPrice.HasValue && price <= _stopPrice.Value)
{
SellMarket(Math.Abs(Position));
ResetPosition();
return;
}
if (_takePrice.HasValue && price >= _takePrice.Value)
{
SellMarket(Math.Abs(Position));
ResetPosition();
return;
}
}
else if (Position < 0)
{
if (_stopPrice.HasValue && price >= _stopPrice.Value)
{
BuyMarket(Math.Abs(Position));
ResetPosition();
return;
}
if (_takePrice.HasValue && price <= _takePrice.Value)
{
BuyMarket(Math.Abs(Position));
ResetPosition();
return;
}
}
}
// Entry: SMA crossover
if (Position == 0)
{
var pip = _pipSize > 0m ? _pipSize : 1m;
if (price > smaValue)
{
BuyMarket();
_entryPrice = price;
_stopPrice = StopLossPips > 0m ? price - StopLossPips * pip : null;
_takePrice = TakeProfitPips > 0m ? price + TakeProfitPips * pip : null;
}
else if (price < smaValue)
{
SellMarket();
_entryPrice = price;
_stopPrice = StopLossPips > 0m ? price + StopLossPips * pip : null;
_takePrice = TakeProfitPips > 0m ? price - TakeProfitPips * pip : null;
}
}
}
private void ResetPosition()
{
_entryPrice = 0m;
_stopPrice = null;
_takePrice = null;
}
private decimal CalculatePipSize()
{
var step = Security?.PriceStep ?? 0m;
if (step <= 0m)
return 0.0001m;
var decimals = Security?.Decimals ?? 0;
return decimals is 5 or 3 ? step * 10m : step;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import Math, TimeSpan
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Indicators import SimpleMovingAverage
from StockSharp.Algo.Strategies import Strategy
class backtesting_trade_assistant_panel_strategy(Strategy):
def __init__(self):
super(backtesting_trade_assistant_panel_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(1)))
self._stop_loss_pips = self.Param("StopLossPips", 50.0)
self._take_profit_pips = self.Param("TakeProfitPips", 100.0)
self._sma = None
self._pip_size = 0.0
self._entry_price = 0.0
self._stop_price = None
self._take_price = None
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
@property
def StopLossPips(self):
return self._stop_loss_pips.Value
@StopLossPips.setter
def StopLossPips(self, value):
self._stop_loss_pips.Value = value
@property
def TakeProfitPips(self):
return self._take_profit_pips.Value
@TakeProfitPips.setter
def TakeProfitPips(self, value):
self._take_profit_pips.Value = value
def OnReseted(self):
super(backtesting_trade_assistant_panel_strategy, self).OnReseted()
self._sma = None
self._pip_size = 0.0
self._entry_price = 0.0
self._stop_price = None
self._take_price = None
def _calculate_pip_size(self):
sec = self.Security
if sec is None:
return 0.0001
step = sec.PriceStep
if step is None or float(step) <= 0:
return 0.0001
step_val = float(step)
decimals = sec.Decimals
if decimals is not None and (int(decimals) == 5 or int(decimals) == 3):
return step_val * 10.0
return step_val
def OnStarted2(self, time):
super(backtesting_trade_assistant_panel_strategy, self).OnStarted2(time)
self._pip_size = self._calculate_pip_size()
self._sma = SimpleMovingAverage()
self._sma.Length = 20
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self._sma, self._process_candle).Start()
def _reset_position(self):
self._entry_price = 0.0
self._stop_price = None
self._take_price = None
def _process_candle(self, candle, sma_value):
if candle.State != CandleStates.Finished:
return
if not self.IsFormed:
return
price = float(candle.ClosePrice)
sma_val = float(sma_value)
# Check stop-loss and take-profit
if self.Position != 0 and self._entry_price > 0:
if self.Position > 0:
if self._stop_price is not None and price <= self._stop_price:
self.SellMarket(abs(float(self.Position)))
self._reset_position()
return
if self._take_price is not None and price >= self._take_price:
self.SellMarket(abs(float(self.Position)))
self._reset_position()
return
elif self.Position < 0:
if self._stop_price is not None and price >= self._stop_price:
self.BuyMarket(abs(float(self.Position)))
self._reset_position()
return
if self._take_price is not None and price <= self._take_price:
self.BuyMarket(abs(float(self.Position)))
self._reset_position()
return
# Entry: SMA crossover
if self.Position == 0:
pip = self._pip_size if self._pip_size > 0 else 1.0
sl_pips = float(self.StopLossPips)
tp_pips = float(self.TakeProfitPips)
if price > sma_val:
self.BuyMarket()
self._entry_price = price
self._stop_price = price - sl_pips * pip if sl_pips > 0 else None
self._take_price = price + tp_pips * pip if tp_pips > 0 else None
elif price < sma_val:
self.SellMarket()
self._entry_price = price
self._stop_price = price + sl_pips * pip if sl_pips > 0 else None
self._take_price = price - tp_pips * pip if tp_pips > 0 else None
def CreateClone(self):
return backtesting_trade_assistant_panel_strategy()