NUp1Down 策略
概述
NUp1Down 策略是 MetaTrader 5 专家顾问 “N bars up, then one bar down”(文件 NUp1Down.mq5)的直接移植版本。
策略只分析 StockSharp 提供的已完成 K 线,当连续多根上涨 K 线后出现一根下跌 K 线时自动建立空头仓位,
适用于希望在 StockSharp Designer、Shell 或 Runner 中自动化经典反转形态的交易者。
交易逻辑
- 仅处理由参数
CandleType指定类型的收盘 K 线。 - 始终保留最近
BarsCount + 1根 K 线。最新 K 线必须收阴(收盘价低于开盘价),这是触发信号的下跌 K 线。 - 之前的
BarsCount根 K 线全部需要收阳。除最旧的一根外,每根 K 线的收盘价都必须高于它前一根的收盘价, 形成阶梯式上涨结构。 - 当上述条件满足并且没有持有空头仓位时,策略会发送市价卖出指令。
- 仓位大小由
RiskPercent控制。算法根据止损距离(以货币计)计算可承受的最大亏损,并确定允许开仓的 手数,确保风险不超过账户权益的设定百分比。Volume仍然是最小下单数量,风险模型只会在此基础上放大仓位。
仓位管理
- 建仓后会立即计算止损与止盈。两者都以点(pip)为单位,并通过
PriceStep转换为价格。对于报价拥有 三位或五位小数的品种,策略会自动调整点值以匹配 MetaTrader 的定义。 - 每根收盘 K 线都会重新计算追踪止损。追踪距离等于
TrailingStopPips,只有当价格至少向有利方向移动TrailingStepPips时才会移动止损。逻辑与原版专家顾问一致:对于空头仓位,止损沿着卖价下移,而策略 不会开多头仓位。 - 在寻找新的入场信号之前,策略会先评估退出条件。一旦触及止损、止盈或者追踪止损被上移至当前卖价之上, 仓位就会被平仓。
参数
| 名称 | 说明 |
|---|---|
BarsCount |
入场前需要的连续上涨 K 线数量(默认 3)。 |
TakeProfitPips |
止盈距离(点),基于入场价计算(默认 50)。 |
StopLossPips |
止损距离(点),基于入场价计算(默认 50)。 |
TrailingStopPips |
追踪止损与当前价格之间的距离(默认 10)。 |
TrailingStepPips |
触发追踪止损移动所需的最小有利波动(默认 5)。 |
RiskPercent |
每笔交易允许承担的账户资金百分比(默认 5)。 |
CandleType |
用于识别形态的 K 线类型/时间框架(默认 1 小时)。 |
使用说明
- 请将
Volume设置为经纪商允许的最小交易手数。风险模型可能会增加下单数量,但不会低于该值。 - 策略在任何时刻仅维持一个综合空头仓位。如果账户中存在多头仓位,系统会先平掉多头再建立空头。
- 策略基于 K 线数据运行。止损或止盈的触发依赖于 K 线的最高价/最低价,因此实际成交时间可能与逐笔行情 略有差异。
- 本次仅提供 C# 版本,Python 版本及其目录暂未创建。代码位于
API/2574/CS/NUp1DownStrategy.cs。
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;
using System.Globalization;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Strategy that sells after a sequence of bullish candles followed by a bearish candle.
/// </summary>
public class NUp1DownStrategy : Strategy
{
private readonly StrategyParam<int> _barsCount;
private readonly StrategyParam<decimal> _takeProfitPips;
private readonly StrategyParam<decimal> _stopLossPips;
private readonly StrategyParam<decimal> _trailingStopPips;
private readonly StrategyParam<decimal> _trailingStepPips;
private readonly StrategyParam<decimal> _riskPercent;
private readonly StrategyParam<DataType> _candleType;
private readonly Queue<(decimal Open, decimal Close)> _recentCandles = new();
private decimal _pipSize;
private decimal? _entryPrice;
private decimal? _activeStopPrice;
private decimal? _activeTakePrice;
/// <summary>
/// Number of consecutive bullish bars required before the bearish setup candle.
/// </summary>
public int BarsCount
{
get => _barsCount.Value;
set => _barsCount.Value = value;
}
/// <summary>
/// Take profit distance in pips.
/// </summary>
public decimal TakeProfitPips
{
get => _takeProfitPips.Value;
set => _takeProfitPips.Value = value;
}
/// <summary>
/// Stop loss distance in pips.
/// </summary>
public decimal StopLossPips
{
get => _stopLossPips.Value;
set => _stopLossPips.Value = value;
}
/// <summary>
/// Trailing stop distance in pips.
/// </summary>
public decimal TrailingStopPips
{
get => _trailingStopPips.Value;
set => _trailingStopPips.Value = value;
}
/// <summary>
/// Trailing stop step in pips.
/// </summary>
public decimal TrailingStepPips
{
get => _trailingStepPips.Value;
set => _trailingStepPips.Value = value;
}
/// <summary>
/// Risk percentage used to size the position.
/// </summary>
public decimal RiskPercent
{
get => _riskPercent.Value;
set => _riskPercent.Value = value;
}
/// <summary>
/// Candle type used for pattern detection.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initializes a new instance of the <see cref="NUp1DownStrategy"/> class.
/// </summary>
public NUp1DownStrategy()
{
Volume = 1m;
_barsCount = Param(nameof(BarsCount), 3)
.SetGreaterThanZero()
.SetDisplay("Bullish Bars", "Number of bullish bars before the down bar", "General")
.SetOptimize(2, 6, 1);
_takeProfitPips = Param(nameof(TakeProfitPips), 50m)
.SetGreaterThanZero()
.SetDisplay("Take Profit (pips)", "Take profit distance in pips", "Risk")
.SetOptimize(20m, 120m, 10m);
_stopLossPips = Param(nameof(StopLossPips), 50m)
.SetGreaterThanZero()
.SetDisplay("Stop Loss (pips)", "Stop loss distance in pips", "Risk")
.SetOptimize(20m, 120m, 10m);
_trailingStopPips = Param(nameof(TrailingStopPips), 10m)
.SetGreaterThanZero()
.SetDisplay("Trailing Stop (pips)", "Trailing stop distance in pips", "Risk")
.SetOptimize(5m, 30m, 5m);
_trailingStepPips = Param(nameof(TrailingStepPips), 5m)
.SetGreaterThanZero()
.SetDisplay("Trailing Step (pips)", "Trailing step before adjusting stop", "Risk")
.SetOptimize(1m, 20m, 1m);
_riskPercent = Param(nameof(RiskPercent), 5m)
.SetGreaterThanZero()
.SetDisplay("Risk %", "Portfolio risk percentage per trade", "Money Management")
.SetOptimize(1m, 10m, 1m);
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Time frame for candle analysis", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_recentCandles.Clear();
_pipSize = 0m;
ResetPositionState();
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_pipSize = CalculatePipSize();
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
UpdateTrailingAndExits(candle);
_recentCandles.Enqueue((candle.OpenPrice, candle.ClosePrice));
while (_recentCandles.Count > BarsCount + 1)
_recentCandles.Dequeue();
if (_recentCandles.Count < BarsCount + 1)
return;
var candles = _recentCandles.ToArray();
var last = candles[^1];
if (last.Close >= last.Open)
return;
var isPattern = true;
for (var i = 1; i <= BarsCount; i++)
{
var index = candles.Length - 1 - i;
var bar = candles[index];
if (bar.Close <= bar.Open)
{
isPattern = false;
break;
}
if (i < BarsCount)
{
var prev = candles[index - 1];
if (bar.Close <= prev.Close)
{
isPattern = false;
break;
}
}
}
if (!isPattern)
return;
if (Position < 0)
return;
SellMarket();
_entryPrice = candle.ClosePrice;
_activeStopPrice = _entryPrice + StopLossPips * _pipSize;
_activeTakePrice = _entryPrice - TakeProfitPips * _pipSize;
this.LogInfo($"Short entry after {BarsCount} bullish bars at {_entryPrice:0.#####}");
}
private void UpdateTrailingAndExits(ICandleMessage candle)
{
if (Position < 0)
{
var volumeToClose = Math.Abs(Position);
if (volumeToClose <= 0m)
return;
if (_activeStopPrice is decimal stop && candle.HighPrice >= stop)
{
BuyMarket();
this.LogInfo($"Short exit by stop-loss at {stop:0.#####}");
ResetPositionState();
return;
}
if (_activeTakePrice is decimal take && candle.LowPrice <= take)
{
BuyMarket();
this.LogInfo($"Short exit by take-profit at {take:0.#####}");
ResetPositionState();
return;
}
if (_activeStopPrice is decimal trailingStop)
{
var trailingDistance = TrailingStopPips * _pipSize;
var trailingStep = TrailingStepPips * _pipSize;
if (trailingDistance <= 0m)
return;
var currentAsk = candle.ClosePrice;
var newStopCandidate = currentAsk + trailingDistance;
if (newStopCandidate + trailingStep < trailingStop)
{
_activeStopPrice = newStopCandidate;
this.LogInfo($"Short trailing stop moved to {_activeStopPrice:0.#####}");
}
}
}
else if (Position == 0)
{
ResetPositionState();
}
}
private decimal CalculatePipSize()
{
if (Security?.PriceStep is decimal step && step > 0m)
{
var decimals = CountDecimalPlaces(step);
return decimals is 3 or 5 ? step * 10m : step;
}
return 1m;
}
private static int CountDecimalPlaces(decimal value)
{
var text = value.ToString(CultureInfo.InvariantCulture);
var separatorIndex = text.IndexOf('.');
return separatorIndex >= 0 ? text.Length - separatorIndex - 1 : 0;
}
private decimal CalculateOrderVolume()
{
var baseVolume = Volume;
var stopDistance = StopLossPips * _pipSize;
if (Portfolio == null || stopDistance <= 0m)
return baseVolume;
var priceStep = Security?.PriceStep ?? 0m;
if (priceStep <= 0m)
return baseVolume;
var capital = Portfolio?.CurrentValue ?? Portfolio?.BeginValue ?? 0m;
if (capital <= 0m)
return baseVolume;
var riskAmount = capital * (RiskPercent / 100m);
if (riskAmount <= 0m)
return baseVolume;
var riskPerUnit = stopDistance;
if (riskPerUnit <= 0m)
return baseVolume;
var volumeFromRisk = riskAmount / riskPerUnit;
if (volumeFromRisk <= 0m)
return baseVolume;
return Math.Max(baseVolume, volumeFromRisk);
}
private void ResetPositionState()
{
_entryPrice = null;
_activeStopPrice = null;
_activeTakePrice = null;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Math
from StockSharp.Messages import DataType, CandleStates, Unit, UnitTypes
from StockSharp.Algo.Strategies import Strategy
class n_up1_down_strategy(Strategy):
def __init__(self):
super(n_up1_down_strategy, self).__init__()
self._bars_count = self.Param("BarsCount", 3)
self._take_profit_pips = self.Param("TakeProfitPips", 50.0)
self._stop_loss_pips = self.Param("StopLossPips", 50.0)
self._trailing_stop_pips = self.Param("TrailingStopPips", 10.0)
self._trailing_step_pips = self.Param("TrailingStepPips", 5.0)
self._risk_percent = self.Param("RiskPercent", 5.0)
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5)))
self._recent_candles = []
self._pip_size = 0.0
self._entry_price = None
self._active_stop_price = None
self._active_take_price = None
@property
def BarsCount(self):
return self._bars_count.Value
@BarsCount.setter
def BarsCount(self, value):
self._bars_count.Value = value
@property
def TakeProfitPips(self):
return self._take_profit_pips.Value
@TakeProfitPips.setter
def TakeProfitPips(self, value):
self._take_profit_pips.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 TrailingStopPips(self):
return self._trailing_stop_pips.Value
@TrailingStopPips.setter
def TrailingStopPips(self, value):
self._trailing_stop_pips.Value = value
@property
def TrailingStepPips(self):
return self._trailing_step_pips.Value
@TrailingStepPips.setter
def TrailingStepPips(self, value):
self._trailing_step_pips.Value = value
@property
def RiskPercent(self):
return self._risk_percent.Value
@RiskPercent.setter
def RiskPercent(self, value):
self._risk_percent.Value = value
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
def _calculate_pip_size(self):
if self.Security is not None and self.Security.PriceStep is not None:
step = float(self.Security.PriceStep)
if step > 0.0:
decimals = self.Security.Decimals if self.Security.Decimals is not None else 0
if decimals == 3 or decimals == 5:
return step * 10.0
return step
return 1.0
def OnStarted2(self, time):
super(n_up1_down_strategy, self).OnStarted2(time)
self._pip_size = self._calculate_pip_size()
self._recent_candles = []
self._entry_price = None
self._active_stop_price = None
self._active_take_price = None
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self.ProcessCandle).Start()
self.StartProtection(
Unit(2000.0, UnitTypes.Absolute),
Unit(1000.0, UnitTypes.Absolute))
def ProcessCandle(self, candle):
if candle.State != CandleStates.Finished:
return
self._update_trailing_and_exits(candle)
open_price = float(candle.OpenPrice)
close_price = float(candle.ClosePrice)
self._recent_candles.append((open_price, close_price))
bars_needed = int(self.BarsCount) + 1
while len(self._recent_candles) > bars_needed:
self._recent_candles.pop(0)
if len(self._recent_candles) < bars_needed:
return
candles = self._recent_candles[:]
last = candles[-1]
# Last candle must be bearish
if last[1] >= last[0]:
return
is_pattern = True
bars_count = int(self.BarsCount)
for i in range(1, bars_count + 1):
index = len(candles) - 1 - i
bar = candles[index]
# Each preceding bar must be bullish
if bar[1] <= bar[0]:
is_pattern = False
break
# Each bullish bar must close higher than the previous
if i < bars_count:
prev = candles[index - 1]
if bar[1] <= prev[1]:
is_pattern = False
break
if not is_pattern:
return
if self.Position < 0:
return
self.SellMarket()
self._entry_price = close_price
self._active_stop_price = self._entry_price + float(self.StopLossPips) * self._pip_size
self._active_take_price = self._entry_price - float(self.TakeProfitPips) * self._pip_size
def _update_trailing_and_exits(self, candle):
if self.Position < 0:
high = float(candle.HighPrice)
low = float(candle.LowPrice)
close = float(candle.ClosePrice)
if self._active_stop_price is not None and high >= self._active_stop_price:
self.BuyMarket()
self._reset_position_state()
return
if self._active_take_price is not None and low <= self._active_take_price:
self.BuyMarket()
self._reset_position_state()
return
if self._active_stop_price is not None:
trailing_distance = float(self.TrailingStopPips) * self._pip_size
trailing_step = float(self.TrailingStepPips) * self._pip_size
if trailing_distance <= 0.0:
return
new_stop_candidate = close + trailing_distance
if new_stop_candidate + trailing_step < self._active_stop_price:
self._active_stop_price = new_stop_candidate
elif self.Position == 0:
self._reset_position_state()
def _reset_position_state(self):
self._entry_price = None
self._active_stop_price = None
self._active_take_price = None
def OnReseted(self):
super(n_up1_down_strategy, self).OnReseted()
self._recent_candles = []
self._pip_size = 0.0
self._reset_position_state()
def CreateClone(self):
return n_up1_down_strategy()