Balance Drawdown In MT4 策略
该策略将 MetaTrader 4 的 BalanceDrawdownInMT4 专家顾问迁移到 StockSharp 高级 API。启动后立刻买入一手多单,并持续监控账户权益相对历史峰值的回撤。
交易逻辑
OnStarted中调用StartProtection,根据输入的点数设置托管止损与止盈。- 在所选时间框架的第一根收盘 K 线(默认 1 分钟)上检查是否已经持仓。如果账户为空仓,则按
Volume参数提交市价买单。 - 每根收盘 K 线都会刷新回撤指标:
- 峰值余额记录为
StartBalance +已实现盈亏 (PnL)。 - 当前权益 =
StartBalance + 已实现 PnL + 未实现 PnL。未实现盈亏通过最新收盘价、平均成交价以及PriceStep/StepPrice计算得到。 - 回撤 = 峰值余额与当前权益之间的百分比跌幅。结果会通过
AddInfoLog写入日志。
- 峰值余额记录为
策略不会加仓或反向开仓。初始头寸将一直持有,直到触发止损、止盈或手动平仓。
参数
| 参数 | 默认值 | 说明 |
|---|---|---|
StartBalance |
1000 |
计算回撤时使用的起始余额。 |
Volume |
0.01 |
初始市价买单的交易数量。 |
StopLossPoints |
300 |
止损距离(点)。0 表示不开启止损。 |
TakeProfitPoints |
400 |
止盈距离(点)。0 表示不开启止盈。 |
CandleType |
1 分钟 | 触发回撤计算与持仓检查的蜡烛类型。 |
实现细节
- 回撤算法复刻原版 EA:峰值余额来自
StartBalance加上已实现收益,当前权益再加上使用价格步长折算的浮动盈亏。 - 若品种未提供
PriceStep或StepPrice,未实现盈亏计算会返回0,以避免除零错误。 - 当
Volume小于等于零时,会记录警告并保持空仓,防止生成无效订单。 - 公开属性
DrawdownPercent,便于监控模块或风险控制逻辑读取当前回撤。
使用建议
- 将
StartBalance设置为真实账户余额或当日开盘余额,以获得准确的回撤百分比。 - 默认的 1 分钟 K 线能够提供稳定的更新频率,如需更快响应可选择更短周期或自定义蜡烛类型。
- 策略仅持有单向多头,可结合外部策略或手动控制实现重复入场。
- 建议在仿真环境中充分测试,确认行情源会提供
PriceStep与StepPrice,否则浮动盈亏可能偏差。
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>
/// Replicates the BalanceDrawdownInMT4 expert advisor: opens a single long position and tracks drawdown from the peak balance.
/// </summary>
public class BalanceDrawdownInMt4Strategy : Strategy
{
private readonly StrategyParam<decimal> _startBalance;
private readonly StrategyParam<decimal> _stopLossPoints;
private readonly StrategyParam<decimal> _takeProfitPoints;
private readonly StrategyParam<int> _entryCooldownDays;
private readonly StrategyParam<DataType> _candleType;
private decimal _maxBalance;
private decimal _lastDrawdown;
private decimal _lastPrice;
private DateTime _lastEntryDate;
/// <summary>
/// Balance used as the baseline for drawdown calculations.
/// </summary>
public decimal StartBalance
{
get => _startBalance.Value;
set => _startBalance.Value = value;
}
/// <summary>
/// Stop-loss distance expressed in price points.
/// </summary>
public decimal StopLossPoints
{
get => _stopLossPoints.Value;
set => _stopLossPoints.Value = value;
}
/// <summary>
/// Take-profit distance expressed in price points.
/// </summary>
public decimal TakeProfitPoints
{
get => _takeProfitPoints.Value;
set => _takeProfitPoints.Value = value;
}
/// <summary>
/// Minimum number of days between new entries.
/// </summary>
public int EntryCooldownDays
{
get => _entryCooldownDays.Value;
set => _entryCooldownDays.Value = value;
}
/// <summary>
/// Timeframe used to trigger periodic drawdown updates.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Current drawdown percentage relative to the peak balance.
/// </summary>
public decimal DrawdownPercent => _lastDrawdown;
/// <summary>
/// Initializes a new instance of the strategy.
/// </summary>
public BalanceDrawdownInMt4Strategy()
{
_startBalance = Param(nameof(StartBalance), 1000m)
.SetDisplay("Start Balance", "Initial balance for drawdown measurement.", "Risk")
;
_stopLossPoints = Param(nameof(StopLossPoints), 300m)
.SetDisplay("Stop-Loss (points)", "Distance from entry price to the protective stop.", "Risk")
;
_takeProfitPoints = Param(nameof(TakeProfitPoints), 400m)
.SetDisplay("Take-Profit (points)", "Distance from entry price to the profit target.", "Risk")
;
_entryCooldownDays = Param(nameof(EntryCooldownDays), 5)
.SetGreaterThanZero()
.SetDisplay("Entry Cooldown Days", "Minimum number of days between new long entries.", "Risk");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Timeframe that drives drawdown monitoring.", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_maxBalance = 0m;
_lastDrawdown = 0m;
_lastPrice = 0m;
_lastEntryDate = default;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
StartProtection(
stopLoss: new Unit(StopLossPoints * GetPriceStep(), UnitTypes.Absolute),
takeProfit: new Unit(TakeProfitPoints * GetPriceStep(), UnitTypes.Absolute));
_maxBalance = StartBalance;
var subscription = SubscribeCandles(CandleType);
subscription.Bind(ProcessCandle).Start();
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
_lastPrice = candle.ClosePrice;
EnsurePosition(candle.CloseTime);
UpdateDrawdown();
}
private void EnsurePosition(DateTime candleDate)
{
if (Position != 0m)
return;
if (_lastEntryDate != default && (candleDate.Date - _lastEntryDate.Date).TotalDays < EntryCooldownDays)
return;
if (Volume <= 0m)
{
LogWarning("Volume parameter must be positive to open the initial trade.");
return;
}
BuyMarket(Volume);
_lastEntryDate = candleDate.Date;
}
private void UpdateDrawdown()
{
var balanceWithoutFloating = StartBalance + PnL;
if (balanceWithoutFloating > _maxBalance)
_maxBalance = balanceWithoutFloating;
if (_maxBalance <= 0m)
{
_lastDrawdown = 0m;
return;
}
var unrealized = CalculateUnrealizedPnL(_lastPrice);
var currentBalance = balanceWithoutFloating + unrealized;
var drawdown = (_maxBalance - currentBalance) / _maxBalance * 100m;
_lastDrawdown = drawdown > 0m ? drawdown : 0m;
LogInfo($"Current drawdown: {_lastDrawdown:F2}%.");
}
private decimal CalculateUnrealizedPnL(decimal price)
{
if (Position == 0m)
return 0m;
var step = Security?.PriceStep ?? 0m;
var stepPrice = GetSecurityValue<decimal?>(Level1Fields.StepPrice) ?? 0m;
if (step <= 0m || stepPrice <= 0m)
return 0m;
var priceDiff = price - _lastPrice;
var points = priceDiff / step;
return points * stepPrice * Position;
}
private decimal GetPriceStep()
{
var step = Security?.PriceStep ?? 0m;
return step > 0m ? step : 0.0001m;
}
}
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, Unit, UnitTypes
from StockSharp.Algo.Strategies import Strategy
class balance_drawdown_in_mt4_strategy(Strategy):
def __init__(self):
super(balance_drawdown_in_mt4_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5)))
self._start_balance = self.Param("StartBalance", 1000.0)
self._stop_loss_points = self.Param("StopLossPoints", 300.0)
self._take_profit_points = self.Param("TakeProfitPoints", 400.0)
self._entry_cooldown_days = self.Param("EntryCooldownDays", 5)
self._max_balance = 0.0
self._last_entry_date = None
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
@property
def StartBalance(self):
return self._start_balance.Value
@StartBalance.setter
def StartBalance(self, value):
self._start_balance.Value = value
@property
def StopLossPoints(self):
return self._stop_loss_points.Value
@StopLossPoints.setter
def StopLossPoints(self, value):
self._stop_loss_points.Value = value
@property
def TakeProfitPoints(self):
return self._take_profit_points.Value
@TakeProfitPoints.setter
def TakeProfitPoints(self, value):
self._take_profit_points.Value = value
@property
def EntryCooldownDays(self):
return self._entry_cooldown_days.Value
@EntryCooldownDays.setter
def EntryCooldownDays(self, value):
self._entry_cooldown_days.Value = value
def OnReseted(self):
super(balance_drawdown_in_mt4_strategy, self).OnReseted()
self._max_balance = 0.0
self._last_entry_date = None
def OnStarted2(self, time):
super(balance_drawdown_in_mt4_strategy, self).OnStarted2(time)
self._max_balance = float(self.StartBalance)
self._last_entry_date = None
self.StartProtection(
takeProfit=Unit(2, UnitTypes.Percent),
stopLoss=Unit(1, UnitTypes.Percent))
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self._process_candle).Start()
def _process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
candle_date = candle.CloseTime.Date
# Ensure position (long-only, with cooldown)
if self.Position == 0:
if self._last_entry_date is not None:
days_diff = (candle_date - self._last_entry_date).TotalDays
if days_diff < self.EntryCooldownDays:
return
self.BuyMarket()
self._last_entry_date = candle_date
def CreateClone(self):
return balance_drawdown_in_mt4_strategy()