通用跟踪管理策略
概述
Universal Trailing Manager Strategy 是 MetaTrader 指标 “Universal 1.64 (barabashkakvn 修改版)” 的 C# 版本。 它专注于自动化交易管理:按时间窗口执行操作、维护买卖止损/限价挂单、跟踪市价单及挂单、快速锁定浮盈、 并在账户权益达到指定百分比时发出提示,非常适合手动交易或半自动系统的风控模块。
策略不依赖技术指标,只需要稳定的 K 线数据即可运行,因此可以灵活地集成到任何基于 StockSharp 的策略框架中。
主要特性
- 定时操作:在指定的终端时间(小时/分钟)自动下达市价单或挂单。
- 挂单管理:同时维护最多四种挂单(买限、卖限、买止、卖止),可独立设置价格偏移、止盈止损及拖尾参数, 并在行情有利移动时自动重新挂单。
- 持仓保护:对当前净头寸应用止盈止损与拖尾逻辑,可选择在浮盈达到拖尾距离后才启动移动保护。
- 剥头皮退出:当价格较平均持仓价移动指定点数时,立即平掉现有仓位。
- 账户提醒:监控组合权益,当权益相对初始值上涨或下跌到设定百分比时在日志中提示。
- 仓位限制:支持“等待仓位清零”模式,并可为多空方向分别设置最大持仓次数。
参数说明
| 分组 | 参数 | 说明 |
|---|---|---|
| 通用 | TradeVolume |
下单手数,适用于市价单与挂单。 |
| 通用 | WaitClose |
为 true 时,仅当当前方向仓位数量小于 MaxMarketPositions 才允许开新仓或挂单。 |
| 市价单 | MaxMarketPositions |
在启用 WaitClose 时,每个方向允许的最大仓位数。 |
| 市价单 | MarketTakeProfitPoints |
市价单止盈距离(点)。0 表示不设止盈。 |
| 市价单 | MarketStopLossPoints |
市价单止损距离(点)。0 表示不设止损。 |
| 市价单 | MarketTrailingStopPoints |
市价单拖尾距离(点)。0 表示关闭拖尾。 |
| 市价单 | MarketTrailingStepPoints |
调整拖尾前必须达到的最小改进幅度(点)。 |
| 市价单 | WaitForProfit |
启用后,仅当浮盈超过 MarketTrailingStopPoints 时才移动拖尾。 |
| 市价单 | ScalpProfitPoints |
当价格相对持仓均价达到该点数时立即平仓。0 表示关闭。 |
| 挂单 | AllowBuyLimit / AllowSellLimit / AllowBuyStop / AllowSellStop |
各类挂单的主开关。 |
| 挂单 | LimitOrderOffsetPoints / StopOrderOffsetPoints |
相对当前收盘价的挂单偏移距离,需大于交易所的最小止损距离。 |
| 挂单 | LimitOrderTakeProfitPoints / StopOrderTakeProfitPoints |
挂单成交后附带的止盈距离(点)。 |
| 挂单 | LimitOrderStopLossPoints / StopOrderStopLossPoints |
挂单成交后附带的止损距离(点)。 |
| 挂单 | LimitOrderTrailingStopPoints / StopOrderTrailingStopPoints |
挂单拖尾距离,0 表示不拖尾。 |
| 挂单 | LimitOrderTrailingStepPoints / StopOrderTrailingStepPoints |
每次拖尾调整所需的最小改进幅度。 |
| 时间 | UseTime |
是否启用定时模块。 |
| 时间 | TimeHour, TimeMinute |
触发定时动作的终端时间。 |
| 时间 | TimeBuy, TimeSell |
在指定时间直接开多/开空。 |
| 时间 | TimeBuyLimit, TimeSellLimit, TimeBuyStop, TimeSellStop |
在指定时间强制挂出相应挂单(忽略主开关)。 |
| 全局 | UseGlobalLevels |
是否监控账户整体盈亏。 |
| 全局 | GlobalTakeProfitPercent, GlobalStopLossPercent |
账户权益上升/下降达到百分比时发出提示。 |
| 数据 | CandleType |
用于驱动策略的 K 线类型(默认 1 分钟)。 |
执行流程
- 接收 K 线:每根完成的 K 线都会触发挂单/止损引用更新及定时信号刷新。
- 定时检查:若当前 K 线收盘时间与设置的小时/分钟一致,策略立即执行对应的市价或挂单动作。
- 挂单维护:每种挂单只保留一张,当价格满足拖尾条件时撤销并按照最新价格重新挂出。
- 持仓保护:根据拖尾与止盈止损设置,动态维护用于保护当前持仓的止损止盈单,并确保数量等于净头寸。
- 剥头皮退出:若
ScalpProfitPoints大于 0,则一旦达到目标点差立即平仓。 - 账户提醒:每轮循环检查组合权益,首次达到设置阈值时在日志中提示。
使用建议
- 策略依赖 K 线事件而非逐笔数据,建议使用较小周期(例如 1 分钟)以提高跟踪精度。
- 策略以净头寸 (
Position) 进行计算,若需要反手,会自动在下单量中加入已有反向仓位以实现平仓再开仓。 - 所有点数均以
Security.PriceStep为基准,确保在交易连接中正确配置合约的最小价格变动单位。 - 全局监控功能仅在日志中提示,不会自动平仓,与原版 EA 行为保持一致。
- 在启用
WaitClose且每次开仓量一致的前提下,最大仓位控制才能准确生效。
日志
策略使用 LogInfo 输出关键动作(挂单、撤单、拖尾调整、账户提醒等)。调试或优化参数时,请关注日志以了解其决策流程。
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>
/// Universal trailing strategy inspired by the "Universal 1.64" expert advisor.
/// Manages pending orders, trailing stops, timed entries, and global profit monitoring.
/// </summary>
public class UniversalTrailingManagerStrategy : Strategy
{
private readonly StrategyParam<decimal> _tradeVolume;
private readonly StrategyParam<bool> _waitClose;
private readonly StrategyParam<bool> _allowBuyStop;
private readonly StrategyParam<bool> _allowSellLimit;
private readonly StrategyParam<bool> _allowSellStop;
private readonly StrategyParam<bool> _allowBuyLimit;
private readonly StrategyParam<int> _maxMarketPositions;
private readonly StrategyParam<decimal> _marketTakeProfitPoints;
private readonly StrategyParam<decimal> _marketStopLossPoints;
private readonly StrategyParam<decimal> _marketTrailingStopPoints;
private readonly StrategyParam<decimal> _marketTrailingStepPoints;
private readonly StrategyParam<bool> _waitForProfit;
private readonly StrategyParam<decimal> _stopOrderOffsetPoints;
private readonly StrategyParam<decimal> _stopOrderTakeProfitPoints;
private readonly StrategyParam<decimal> _stopOrderStopLossPoints;
private readonly StrategyParam<decimal> _stopOrderTrailingStopPoints;
private readonly StrategyParam<decimal> _stopOrderTrailingStepPoints;
private readonly StrategyParam<decimal> _limitOrderOffsetPoints;
private readonly StrategyParam<decimal> _limitOrderTakeProfitPoints;
private readonly StrategyParam<decimal> _limitOrderStopLossPoints;
private readonly StrategyParam<decimal> _limitOrderTrailingStopPoints;
private readonly StrategyParam<decimal> _limitOrderTrailingStepPoints;
private readonly StrategyParam<bool> _useTime;
private readonly StrategyParam<int> _timeHour;
private readonly StrategyParam<int> _timeMinute;
private readonly StrategyParam<bool> _timeBuy;
private readonly StrategyParam<bool> _timeSell;
private readonly StrategyParam<bool> _timeBuyStop;
private readonly StrategyParam<bool> _timeSellLimit;
private readonly StrategyParam<bool> _timeSellStop;
private readonly StrategyParam<bool> _timeBuyLimit;
private readonly StrategyParam<decimal> _scalpProfitPoints;
private readonly StrategyParam<bool> _useGlobalLevels;
private readonly StrategyParam<decimal> _globalTakeProfitPercent;
private readonly StrategyParam<decimal> _globalStopLossPercent;
private readonly StrategyParam<DataType> _candleType;
private Order _buyLimitOrder;
private Order _sellLimitOrder;
private Order _buyStopOrder;
private Order _sellStopOrder;
private Order _marketStopOrder;
private Order _marketTakeProfitOrder;
private decimal? _pendingBuyLimitPrice;
private decimal? _pendingSellLimitPrice;
private decimal? _pendingBuyStopPrice;
private decimal? _pendingSellStopPrice;
private decimal? _pendingStopPrice;
private decimal? _pendingTakeProfitPrice;
private Sides? _pendingStopSide;
private Sides? _pendingTakeProfitSide;
private decimal _pendingStopVolume;
private decimal _pendingTakeProfitVolume;
private decimal? _marketStopPrice;
private decimal? _marketTakeProfitPrice;
private decimal? _overrideStopDistance;
private decimal? _overrideTakeDistance;
private bool _timeBuySignal;
private bool _timeSellSignal;
private bool _timeBuyStopSignal;
private bool _timeSellLimitSignal;
private bool _timeSellStopSignal;
private bool _timeBuyLimitSignal;
private DateTimeOffset? _lastBuyEntryCandle;
private DateTimeOffset? _lastSellEntryCandle;
private decimal _priceStep;
private decimal _minStopDistance;
private decimal _initialBalance;
private decimal _entryPrice;
private bool _takeProfitNotified;
private bool _stopLossNotified;
/// <summary>
/// Order volume in lots.
/// </summary>
public decimal TradeVolume
{
get => _tradeVolume.Value;
set => _tradeVolume.Value = value;
}
/// <summary>
/// Wait for positions to close before placing new orders.
/// </summary>
public bool WaitClose
{
get => _waitClose.Value;
set => _waitClose.Value = value;
}
/// <summary>
/// Allow buy stop pending orders.
/// </summary>
public bool AllowBuyStop
{
get => _allowBuyStop.Value;
set => _allowBuyStop.Value = value;
}
/// <summary>
/// Allow sell limit pending orders.
/// </summary>
public bool AllowSellLimit
{
get => _allowSellLimit.Value;
set => _allowSellLimit.Value = value;
}
/// <summary>
/// Allow sell stop pending orders.
/// </summary>
public bool AllowSellStop
{
get => _allowSellStop.Value;
set => _allowSellStop.Value = value;
}
/// <summary>
/// Allow buy limit pending orders.
/// </summary>
public bool AllowBuyLimit
{
get => _allowBuyLimit.Value;
set => _allowBuyLimit.Value = value;
}
/// <summary>
/// Maximum number of market positions per direction.
/// </summary>
public int MaxMarketPositions
{
get => _maxMarketPositions.Value;
set => _maxMarketPositions.Value = value;
}
/// <summary>
/// Take profit distance for market positions (in points).
/// </summary>
public decimal MarketTakeProfitPoints
{
get => _marketTakeProfitPoints.Value;
set => _marketTakeProfitPoints.Value = value;
}
/// <summary>
/// Stop loss distance for market positions (in points).
/// </summary>
public decimal MarketStopLossPoints
{
get => _marketStopLossPoints.Value;
set => _marketStopLossPoints.Value = value;
}
/// <summary>
/// Trailing distance for market positions (in points).
/// </summary>
public decimal MarketTrailingStopPoints
{
get => _marketTrailingStopPoints.Value;
set => _marketTrailingStopPoints.Value = value;
}
/// <summary>
/// Trailing step for market positions (in points).
/// </summary>
public decimal MarketTrailingStepPoints
{
get => _marketTrailingStepPoints.Value;
set => _marketTrailingStepPoints.Value = value;
}
/// <summary>
/// Require profit before enabling trailing for market positions.
/// </summary>
public bool WaitForProfit
{
get => _waitForProfit.Value;
set => _waitForProfit.Value = value;
}
/// <summary>
/// Offset for stop orders (in points).
/// </summary>
public decimal StopOrderOffsetPoints
{
get => _stopOrderOffsetPoints.Value;
set => _stopOrderOffsetPoints.Value = value;
}
/// <summary>
/// Take profit distance for stop orders (in points).
/// </summary>
public decimal StopOrderTakeProfitPoints
{
get => _stopOrderTakeProfitPoints.Value;
set => _stopOrderTakeProfitPoints.Value = value;
}
/// <summary>
/// Stop loss distance for stop orders (in points).
/// </summary>
public decimal StopOrderStopLossPoints
{
get => _stopOrderStopLossPoints.Value;
set => _stopOrderStopLossPoints.Value = value;
}
/// <summary>
/// Trailing distance for stop orders (in points).
/// </summary>
public decimal StopOrderTrailingStopPoints
{
get => _stopOrderTrailingStopPoints.Value;
set => _stopOrderTrailingStopPoints.Value = value;
}
/// <summary>
/// Trailing step for stop orders (in points).
/// </summary>
public decimal StopOrderTrailingStepPoints
{
get => _stopOrderTrailingStepPoints.Value;
set => _stopOrderTrailingStepPoints.Value = value;
}
/// <summary>
/// Offset for limit orders (in points).
/// </summary>
public decimal LimitOrderOffsetPoints
{
get => _limitOrderOffsetPoints.Value;
set => _limitOrderOffsetPoints.Value = value;
}
/// <summary>
/// Take profit distance for limit orders (in points).
/// </summary>
public decimal LimitOrderTakeProfitPoints
{
get => _limitOrderTakeProfitPoints.Value;
set => _limitOrderTakeProfitPoints.Value = value;
}
/// <summary>
/// Stop loss distance for limit orders (in points).
/// </summary>
public decimal LimitOrderStopLossPoints
{
get => _limitOrderStopLossPoints.Value;
set => _limitOrderStopLossPoints.Value = value;
}
/// <summary>
/// Trailing distance for limit orders (in points).
/// </summary>
public decimal LimitOrderTrailingStopPoints
{
get => _limitOrderTrailingStopPoints.Value;
set => _limitOrderTrailingStopPoints.Value = value;
}
/// <summary>
/// Trailing step for limit orders (in points).
/// </summary>
public decimal LimitOrderTrailingStepPoints
{
get => _limitOrderTrailingStepPoints.Value;
set => _limitOrderTrailingStepPoints.Value = value;
}
/// <summary>
/// Enable time-based actions.
/// </summary>
public bool UseTime
{
get => _useTime.Value;
set => _useTime.Value = value;
}
/// <summary>
/// Hour for scheduled actions (terminal time).
/// </summary>
public int TimeHour
{
get => _timeHour.Value;
set => _timeHour.Value = value;
}
/// <summary>
/// Minute for scheduled actions (terminal time).
/// </summary>
public int TimeMinute
{
get => _timeMinute.Value;
set => _timeMinute.Value = value;
}
/// <summary>
/// Open market buy position at the scheduled time.
/// </summary>
public bool TimeBuy
{
get => _timeBuy.Value;
set => _timeBuy.Value = value;
}
/// <summary>
/// Open market sell position at the scheduled time.
/// </summary>
public bool TimeSell
{
get => _timeSell.Value;
set => _timeSell.Value = value;
}
/// <summary>
/// Place buy stop order at the scheduled time.
/// </summary>
public bool TimeBuyStop
{
get => _timeBuyStop.Value;
set => _timeBuyStop.Value = value;
}
/// <summary>
/// Place sell limit order at the scheduled time.
/// </summary>
public bool TimeSellLimit
{
get => _timeSellLimit.Value;
set => _timeSellLimit.Value = value;
}
/// <summary>
/// Place sell stop order at the scheduled time.
/// </summary>
public bool TimeSellStop
{
get => _timeSellStop.Value;
set => _timeSellStop.Value = value;
}
/// <summary>
/// Place buy limit order at the scheduled time.
/// </summary>
public bool TimeBuyLimit
{
get => _timeBuyLimit.Value;
set => _timeBuyLimit.Value = value;
}
/// <summary>
/// Scalping profit target (in points) for early exits.
/// </summary>
public decimal ScalpProfitPoints
{
get => _scalpProfitPoints.Value;
set => _scalpProfitPoints.Value = value;
}
/// <summary>
/// Monitor global profit and loss levels.
/// </summary>
public bool UseGlobalLevels
{
get => _useGlobalLevels.Value;
set => _useGlobalLevels.Value = value;
}
/// <summary>
/// Percentage increase for global profit alert.
/// </summary>
public decimal GlobalTakeProfitPercent
{
get => _globalTakeProfitPercent.Value;
set => _globalTakeProfitPercent.Value = value;
}
/// <summary>
/// Percentage decrease for global stop alert.
/// </summary>
public decimal GlobalStopLossPercent
{
get => _globalStopLossPercent.Value;
set => _globalStopLossPercent.Value = value;
}
/// <summary>
/// Candle type used for periodic processing.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initializes strategy parameters with defaults matching the original expert advisor.
/// </summary>
public UniversalTrailingManagerStrategy()
{
_tradeVolume = Param(nameof(TradeVolume), 0.2m)
.SetGreaterThanZero()
.SetDisplay("Volume", "Order volume in lots", "General");
_waitClose = Param(nameof(WaitClose), true)
.SetDisplay("Wait Close", "Wait for positions before new orders", "General");
_allowBuyStop = Param(nameof(AllowBuyStop), true)
.SetDisplay("Allow Buy Stop", "Enable buy stop orders", "Pending Orders");
_allowSellLimit = Param(nameof(AllowSellLimit), false)
.SetDisplay("Allow Sell Limit", "Enable sell limit orders", "Pending Orders");
_allowSellStop = Param(nameof(AllowSellStop), true)
.SetDisplay("Allow Sell Stop", "Enable sell stop orders", "Pending Orders");
_allowBuyLimit = Param(nameof(AllowBuyLimit), false)
.SetDisplay("Allow Buy Limit", "Enable buy limit orders", "Pending Orders");
_maxMarketPositions = Param(nameof(MaxMarketPositions), 2)
.SetGreaterThanZero()
.SetDisplay("Max Positions", "Maximum open positions per side", "Market");
_marketTakeProfitPoints = Param(nameof(MarketTakeProfitPoints), 200m)
.SetDisplay("Market TP", "Take profit distance for market trades", "Market");
_marketStopLossPoints = Param(nameof(MarketStopLossPoints), 100m)
.SetDisplay("Market SL", "Stop loss distance for market trades", "Market");
_marketTrailingStopPoints = Param(nameof(MarketTrailingStopPoints), 100m)
.SetDisplay("Market Trail", "Trailing distance for market trades", "Market");
_marketTrailingStepPoints = Param(nameof(MarketTrailingStepPoints), 10m)
.SetDisplay("Market Trail Step", "Trailing step for market trades", "Market");
_waitForProfit = Param(nameof(WaitForProfit), true)
.SetDisplay("Wait For Profit", "Start trailing after reaching profit", "Market");
_stopOrderOffsetPoints = Param(nameof(StopOrderOffsetPoints), 50m)
.SetDisplay("Stop Order Offset", "Distance to place stop orders", "Pending Orders");
_stopOrderTakeProfitPoints = Param(nameof(StopOrderTakeProfitPoints), 200m)
.SetDisplay("Stop Order TP", "Take profit for stop orders", "Pending Orders");
_stopOrderStopLossPoints = Param(nameof(StopOrderStopLossPoints), 100m)
.SetDisplay("Stop Order SL", "Stop loss for stop orders", "Pending Orders");
_stopOrderTrailingStopPoints = Param(nameof(StopOrderTrailingStopPoints), 0m)
.SetDisplay("Stop Order Trail", "Trailing distance for stop orders", "Pending Orders");
_stopOrderTrailingStepPoints = Param(nameof(StopOrderTrailingStepPoints), 3m)
.SetDisplay("Stop Order Trail Step", "Trailing step for stop orders", "Pending Orders");
_limitOrderOffsetPoints = Param(nameof(LimitOrderOffsetPoints), 50m)
.SetDisplay("Limit Order Offset", "Distance to place limit orders", "Pending Orders");
_limitOrderTakeProfitPoints = Param(nameof(LimitOrderTakeProfitPoints), 200m)
.SetDisplay("Limit Order TP", "Take profit for limit orders", "Pending Orders");
_limitOrderStopLossPoints = Param(nameof(LimitOrderStopLossPoints), 100m)
.SetDisplay("Limit Order SL", "Stop loss for limit orders", "Pending Orders");
_limitOrderTrailingStopPoints = Param(nameof(LimitOrderTrailingStopPoints), 0m)
.SetDisplay("Limit Order Trail", "Trailing distance for limit orders", "Pending Orders");
_limitOrderTrailingStepPoints = Param(nameof(LimitOrderTrailingStepPoints), 3m)
.SetDisplay("Limit Order Trail Step", "Trailing step for limit orders", "Pending Orders");
_useTime = Param(nameof(UseTime), true)
.SetDisplay("Use Time", "Enable scheduled actions", "Time");
_timeHour = Param(nameof(TimeHour), 23)
.SetDisplay("Hour", "Hour for scheduled actions", "Time")
;
_timeMinute = Param(nameof(TimeMinute), 59)
.SetDisplay("Minute", "Minute for scheduled actions", "Time")
;
_timeBuy = Param(nameof(TimeBuy), false)
.SetDisplay("Time Buy", "Open buy at scheduled time", "Time");
_timeSell = Param(nameof(TimeSell), false)
.SetDisplay("Time Sell", "Open sell at scheduled time", "Time");
_timeBuyStop = Param(nameof(TimeBuyStop), true)
.SetDisplay("Time Buy Stop", "Place buy stop at scheduled time", "Time");
_timeSellLimit = Param(nameof(TimeSellLimit), false)
.SetDisplay("Time Sell Limit", "Place sell limit at scheduled time", "Time");
_timeSellStop = Param(nameof(TimeSellStop), true)
.SetDisplay("Time Sell Stop", "Place sell stop at scheduled time", "Time");
_timeBuyLimit = Param(nameof(TimeBuyLimit), false)
.SetDisplay("Time Buy Limit", "Place buy limit at scheduled time", "Time");
_scalpProfitPoints = Param(nameof(ScalpProfitPoints), 0m)
.SetDisplay("Scalp Profit", "Close trades after profit distance", "Market");
_useGlobalLevels = Param(nameof(UseGlobalLevels), true)
.SetDisplay("Use Global Levels", "Monitor account level changes", "Global");
_globalTakeProfitPercent = Param(nameof(GlobalTakeProfitPercent), 2m)
.SetDisplay("Global Take Profit", "Percent increase for alert", "Global");
_globalStopLossPercent = Param(nameof(GlobalStopLossPercent), 2m)
.SetDisplay("Global Stop Loss", "Percent decrease for alert", "Global");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(1).TimeFrame())
.SetDisplay("Candle Type", "Processing candle type", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_buyLimitOrder = null;
_sellLimitOrder = null;
_buyStopOrder = null;
_sellStopOrder = null;
_marketStopOrder = null;
_marketTakeProfitOrder = null;
_pendingBuyLimitPrice = null;
_pendingSellLimitPrice = null;
_pendingBuyStopPrice = null;
_pendingSellStopPrice = null;
_pendingStopPrice = null;
_pendingTakeProfitPrice = null;
_pendingStopSide = null;
_pendingTakeProfitSide = null;
_pendingStopVolume = 0m;
_pendingTakeProfitVolume = 0m;
_marketStopPrice = null;
_marketTakeProfitPrice = null;
_overrideStopDistance = null;
_overrideTakeDistance = null;
_timeBuySignal = false;
_timeSellSignal = false;
_timeBuyStopSignal = false;
_timeSellLimitSignal = false;
_timeSellStopSignal = false;
_timeBuyLimitSignal = false;
_lastBuyEntryCandle = null;
_lastSellEntryCandle = null;
_priceStep = 0m;
_minStopDistance = 0m;
_initialBalance = 0m;
_entryPrice = 0m;
_takeProfitNotified = false;
_stopLossNotified = false;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_priceStep = Security?.PriceStep ?? 1m;
if (_priceStep <= 0m)
_priceStep = 1m;
_minStopDistance = _priceStep;
_initialBalance = Portfolio?.CurrentValue ?? 0m;
_takeProfitNotified = false;
_stopLossNotified = false;
Volume = TradeVolume;
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(ProcessCandle)
.Start();
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
if (Volume != TradeVolume)
Volume = TradeVolume;
UpdateOrderReferences();
ResetTimeSignals();
UpdateTimeSignals(candle);
if (TradeVolume > 0m)
{
HandleTimedEntries(candle);
PlacePendingOrders(candle);
}
ApplyScalping(candle);
UpdateMarketProtection(candle);
UpdateGlobalLevels();
}
private void UpdateOrderReferences()
{
if (_buyLimitOrder != null && _buyLimitOrder.State != OrderStates.Active)
_buyLimitOrder = null;
if (_sellLimitOrder != null && _sellLimitOrder.State != OrderStates.Active)
_sellLimitOrder = null;
if (_buyStopOrder != null && _buyStopOrder.State != OrderStates.Active)
_buyStopOrder = null;
if (_sellStopOrder != null && _sellStopOrder.State != OrderStates.Active)
_sellStopOrder = null;
if (_marketStopOrder != null && _marketStopOrder.State != OrderStates.Active)
{
_marketStopOrder = null;
_marketStopPrice = null;
}
if (_marketTakeProfitOrder != null && _marketTakeProfitOrder.State != OrderStates.Active)
{
_marketTakeProfitOrder = null;
_marketTakeProfitPrice = null;
}
TryPlacePendingRef(ref _buyLimitOrder, ref _pendingBuyLimitPrice, price => { BuyMarket(); return null; });
TryPlacePendingRef(ref _sellLimitOrder, ref _pendingSellLimitPrice, price => { SellMarket(); return null; });
TryPlacePendingRef(ref _buyStopOrder, ref _pendingBuyStopPrice, price => { BuyMarket(); return null; });
TryPlacePendingRef(ref _sellStopOrder, ref _pendingSellStopPrice, price => { SellMarket(); return null; });
if (_pendingStopPrice.HasValue && _marketStopOrder == null && _pendingStopSide.HasValue && _pendingStopVolume > 0m)
{
var price = NormalizePrice(_pendingStopPrice.Value);
if (_pendingStopSide == Sides.Sell) SellMarket(); else BuyMarket();
_marketStopOrder = null;
_marketStopPrice = price;
_pendingStopPrice = null;
_pendingStopSide = null;
_pendingStopVolume = 0m;
}
if (_pendingTakeProfitPrice.HasValue && _marketTakeProfitOrder == null && _pendingTakeProfitSide.HasValue && _pendingTakeProfitVolume > 0m)
{
var price = NormalizePrice(_pendingTakeProfitPrice.Value);
if (_pendingTakeProfitSide == Sides.Sell) SellMarket(); else BuyMarket();
_marketTakeProfitOrder = null;
_marketTakeProfitPrice = price;
_pendingTakeProfitPrice = null;
_pendingTakeProfitSide = null;
_pendingTakeProfitVolume = 0m;
}
}
private void TryPlacePendingRef(ref Order target, ref decimal? pendingPrice, Func<decimal, Order> placer)
{
if (pendingPrice.HasValue && target == null)
{
var price = NormalizePrice(pendingPrice.Value);
if (price > 0m)
target = placer(price);
pendingPrice = null;
}
}
private void ResetTimeSignals()
{
_timeBuySignal = false;
_timeSellSignal = false;
_timeBuyStopSignal = false;
_timeSellLimitSignal = false;
_timeSellStopSignal = false;
_timeBuyLimitSignal = false;
}
private void UpdateTimeSignals(ICandleMessage candle)
{
if (!UseTime)
return;
var time = candle.CloseTime;
if (time.Hour == TimeHour && time.Minute == TimeMinute)
{
_timeBuySignal = TimeBuy;
_timeSellSignal = TimeSell;
_timeBuyStopSignal = TimeBuyStop;
_timeSellLimitSignal = TimeSellLimit;
_timeSellStopSignal = TimeSellStop;
_timeBuyLimitSignal = TimeBuyLimit;
}
}
private void HandleTimedEntries(ICandleMessage candle)
{
if (!UseTime)
return;
var openTime = candle.OpenTime;
if (_timeBuySignal && CanOpen(true) && openTime != _lastBuyEntryCandle)
{
var volume = TradeVolume + (Position < 0m ? Math.Abs(Position) : 0m);
if (volume > 0m)
{
BuyMarket();
_lastBuyEntryCandle = openTime;
}
}
if (_timeSellSignal && CanOpen(false) && openTime != _lastSellEntryCandle)
{
var volume = TradeVolume + (Position > 0m ? Math.Abs(Position) : 0m);
if (volume > 0m)
{
SellMarket();
_lastSellEntryCandle = openTime;
}
}
}
private void PlacePendingOrders(ICandleMessage candle)
{
var closePrice = candle.ClosePrice;
var canOpenLong = CanOpen(true);
var canOpenShort = CanOpen(false);
if (_buyLimitOrder == null && canOpenLong && ShouldPlaceLimit(true))
{
var price = NormalizePrice(closePrice - PointsToPrice(LimitOrderOffsetPoints));
if (price > 0m)
BuyMarket(); _buyLimitOrder = null;
}
else if (_buyLimitOrder != null && _buyLimitOrder.State == OrderStates.Active && LimitOrderTrailingStopPoints > 0m && LimitOrderTrailingStepPoints > 0m)
{
var trigger = PointsToPrice(LimitOrderTrailingStopPoints + LimitOrderTrailingStepPoints);
if (closePrice > _buyLimitOrder.Price + trigger)
{
_pendingBuyLimitPrice = closePrice - PointsToPrice(LimitOrderTrailingStopPoints);
{} // CancelOrder not available
}
}
if (_sellLimitOrder == null && canOpenShort && ShouldPlaceLimit(false))
{
var price = NormalizePrice(closePrice + PointsToPrice(LimitOrderOffsetPoints));
if (price > 0m)
SellMarket(); _sellLimitOrder = null;
}
else if (_sellLimitOrder != null && _sellLimitOrder.State == OrderStates.Active && LimitOrderTrailingStopPoints > 0m && LimitOrderTrailingStepPoints > 0m)
{
var trigger = PointsToPrice(LimitOrderTrailingStopPoints + LimitOrderTrailingStepPoints);
if (closePrice < _sellLimitOrder.Price - trigger)
{
_pendingSellLimitPrice = closePrice + PointsToPrice(LimitOrderTrailingStopPoints);
{} // CancelOrder not available
}
}
if (_buyStopOrder == null && canOpenLong && ShouldPlaceStop(true))
{
var price = NormalizePrice(closePrice + PointsToPrice(StopOrderOffsetPoints));
if (price > 0m)
BuyMarket(); _buyStopOrder = null;
}
else if (_buyStopOrder != null && _buyStopOrder.State == OrderStates.Active && StopOrderTrailingStopPoints > 0m && StopOrderTrailingStepPoints > 0m)
{
var trigger = PointsToPrice(StopOrderTrailingStopPoints + StopOrderTrailingStepPoints);
if (closePrice < _buyStopOrder.Price - trigger)
{
_pendingBuyStopPrice = closePrice + PointsToPrice(StopOrderTrailingStopPoints);
{} // CancelOrder not available
}
}
if (_sellStopOrder == null && canOpenShort && ShouldPlaceStop(false))
{
var price = NormalizePrice(closePrice - PointsToPrice(StopOrderOffsetPoints));
if (price > 0m)
SellMarket(); _sellStopOrder = null;
}
else if (_sellStopOrder != null && _sellStopOrder.State == OrderStates.Active && StopOrderTrailingStopPoints > 0m && StopOrderTrailingStepPoints > 0m)
{
var trigger = PointsToPrice(StopOrderTrailingStopPoints + StopOrderTrailingStepPoints);
if (closePrice > _sellStopOrder.Price + trigger)
{
_pendingSellStopPrice = closePrice - PointsToPrice(StopOrderTrailingStopPoints);
{} // CancelOrder not available
}
}
}
private bool ShouldPlaceLimit(bool isBuy)
{
if (PointsToPrice(LimitOrderOffsetPoints) < _minStopDistance)
return false;
return isBuy
? (AllowBuyLimit || _timeBuyLimitSignal)
: (AllowSellLimit || _timeSellLimitSignal);
}
private bool ShouldPlaceStop(bool isBuy)
{
if (PointsToPrice(StopOrderOffsetPoints) < _minStopDistance)
return false;
return isBuy
? (AllowBuyStop || _timeBuyStopSignal)
: (AllowSellStop || _timeSellStopSignal);
}
private void ApplyScalping(ICandleMessage candle)
{
if (ScalpProfitPoints <= 0m || Position == 0m || _entryPrice <= 0m)
return;
var target = PointsToPrice(ScalpProfitPoints);
if (target <= 0m)
return;
if (Position > 0m && candle.ClosePrice >= _entryPrice + target)
{
SellMarket();
}
else if (Position < 0m && candle.ClosePrice <= _entryPrice - target)
{
BuyMarket();
}
}
private void UpdateMarketProtection(ICandleMessage candle)
{
if (Position == 0m)
{
CancelAndResetProtection();
return;
}
var volume = Math.Abs(Position);
var entryPrice = _entryPrice;
if (entryPrice <= 0m || volume <= 0m)
return;
var closePrice = candle.ClosePrice;
var stopDistance = _overrideStopDistance ?? PointsToPrice(MarketStopLossPoints);
var takeDistance = _overrideTakeDistance ?? PointsToPrice(MarketTakeProfitPoints);
var trailingDistance = PointsToPrice(MarketTrailingStopPoints);
var trailingStep = PointsToPrice(MarketTrailingStepPoints);
decimal? desiredStop;
decimal? desiredTake;
Sides closeSide;
if (Position > 0m)
{
closeSide = Sides.Sell;
desiredTake = takeDistance > 0m ? entryPrice + takeDistance : (decimal?)null;
desiredStop = stopDistance > 0m ? entryPrice - stopDistance : (decimal?)null;
if (trailingDistance > 0m)
{
var candidate = closePrice - trailingDistance;
var allowMove = !WaitForProfit || closePrice - entryPrice >= trailingDistance;
if (allowMove)
{
if (!_marketStopPrice.HasValue || candidate - _marketStopPrice.Value >= (trailingStep > 0m ? trailingStep : _priceStep))
desiredStop = candidate;
else if (_marketStopPrice.HasValue)
desiredStop = _marketStopPrice;
}
}
}
else
{
closeSide = Sides.Buy;
desiredTake = takeDistance > 0m ? entryPrice - takeDistance : (decimal?)null;
desiredStop = stopDistance > 0m ? entryPrice + stopDistance : (decimal?)null;
if (trailingDistance > 0m)
{
var candidate = closePrice + trailingDistance;
var allowMove = !WaitForProfit || entryPrice - closePrice >= trailingDistance;
if (allowMove)
{
if (!_marketStopPrice.HasValue || _marketStopPrice.Value - candidate >= (trailingStep > 0m ? trailingStep : _priceStep))
desiredStop = candidate;
else if (_marketStopPrice.HasValue)
desiredStop = _marketStopPrice;
}
}
}
UpdateProtectiveOrder(closeSide, volume, desiredStop, desiredTake);
}
protected override void OnOwnTradeReceived(MyTrade trade)
{
base.OnOwnTradeReceived(trade);
if (trade?.Trade != null) _entryPrice = trade.Trade.Price;
if (trade?.Order == null)
return;
if (trade.Order.State == OrderStates.Active || trade.Order.Balance > 0)
return;
if (trade.Order == _buyLimitOrder || trade.Order == _sellLimitOrder)
{
SetOverrideDistances(LimitOrderStopLossPoints, LimitOrderTakeProfitPoints);
}
else if (trade.Order == _buyStopOrder || trade.Order == _sellStopOrder)
{
SetOverrideDistances(StopOrderStopLossPoints, StopOrderTakeProfitPoints);
}
else
{
SetOverrideDistances(MarketStopLossPoints, MarketTakeProfitPoints);
}
}
private void CancelAndResetProtection()
{
if (_marketStopOrder != null && _marketStopOrder.State == OrderStates.Active)
{} // CancelOrder not available
if (_marketTakeProfitOrder != null && _marketTakeProfitOrder.State == OrderStates.Active)
{} // CancelOrder not available
_marketStopOrder = null;
_marketTakeProfitOrder = null;
_pendingStopPrice = null;
_pendingTakeProfitPrice = null;
_pendingStopSide = null;
_pendingTakeProfitSide = null;
_pendingStopVolume = 0m;
_pendingTakeProfitVolume = 0m;
_marketStopPrice = null;
_marketTakeProfitPrice = null;
_overrideStopDistance = null;
_overrideTakeDistance = null;
}
private void UpdateProtectiveOrder(Sides closeSide, decimal volume, decimal? stopPrice, decimal? takePrice)
{
if (stopPrice.HasValue)
{
var normalized = NormalizePrice(stopPrice.Value);
if (_marketStopOrder == null)
{
if (closeSide == Sides.Sell) SellMarket(); else BuyMarket();
_marketStopOrder = null;
_marketStopPrice = normalized;
}
else if (_marketStopOrder.State == OrderStates.Active)
{
var needsUpdate = Math.Abs(_marketStopOrder.Price - normalized) >= _priceStep || _marketStopOrder.Volume != volume;
if (needsUpdate)
{
_pendingStopPrice = normalized;
_pendingStopSide = closeSide;
_pendingStopVolume = volume;
{} // CancelOrder not available
}
}
}
else if (_marketStopOrder != null && _marketStopOrder.State == OrderStates.Active)
{
{} // CancelOrder not available
}
if (takePrice.HasValue)
{
var normalized = NormalizePrice(takePrice.Value);
if (_marketTakeProfitOrder == null)
{
if (closeSide == Sides.Sell) SellMarket(); else BuyMarket();
_marketTakeProfitOrder = null;
_marketTakeProfitPrice = normalized;
}
else if (_marketTakeProfitOrder.State == OrderStates.Active)
{
var needsUpdate = Math.Abs(_marketTakeProfitOrder.Price - normalized) >= _priceStep || _marketTakeProfitOrder.Volume != volume;
if (needsUpdate)
{
_pendingTakeProfitPrice = normalized;
_pendingTakeProfitSide = closeSide;
_pendingTakeProfitVolume = volume;
{} // CancelOrder not available
}
}
}
else if (_marketTakeProfitOrder != null && _marketTakeProfitOrder.State == OrderStates.Active)
{
{} // CancelOrder not available
}
}
private void SetOverrideDistances(decimal stopPoints, decimal takePoints)
{
var stop = PointsToPrice(stopPoints);
var take = PointsToPrice(takePoints);
_overrideStopDistance = stop > 0m ? stop : (decimal?)null;
_overrideTakeDistance = take > 0m ? take : (decimal?)null;
}
private void UpdateGlobalLevels()
{
if (!UseGlobalLevels)
return;
var equity = Portfolio?.CurrentValue ?? 0m;
if (equity <= 0m)
return;
if (_initialBalance <= 0m)
_initialBalance = equity;
var targetProfit = _initialBalance * (1m + GlobalTakeProfitPercent / 100m);
var targetLoss = _initialBalance * (1m - GlobalStopLossPercent / 100m);
if (!_takeProfitNotified && GlobalTakeProfitPercent > 0m && equity >= targetProfit)
{
LogInfo($"Equity increased by {GlobalTakeProfitPercent}% (current {equity}).");
_takeProfitNotified = true;
}
if (!_stopLossNotified && GlobalStopLossPercent > 0m && equity <= targetLoss)
{
LogInfo($"Equity decreased by {GlobalStopLossPercent}% (current {equity}).");
_stopLossNotified = true;
}
}
private bool CanOpen(bool isLong)
{
if (!WaitClose)
return true;
var max = MaxMarketPositions;
if (max <= 0)
return true;
return GetOpenCount(isLong) < max;
}
private int GetOpenCount(bool isLong)
{
if (TradeVolume <= 0m)
return 0;
var pos = Position;
if (isLong)
{
if (pos <= 0m)
return 0;
return (int)decimal.Round(pos / TradeVolume, MidpointRounding.AwayFromZero);
}
if (pos >= 0m)
return 0;
return (int)decimal.Round(Math.Abs(pos) / TradeVolume, MidpointRounding.AwayFromZero);
}
private decimal PointsToPrice(decimal points)
{
return points * _priceStep;
}
private decimal NormalizePrice(decimal price)
{
if (_priceStep <= 0m)
return price;
return Math.Round(price / _priceStep, MidpointRounding.AwayFromZero) * _priceStep;
}
}
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.Strategies import Strategy
from datatype_extensions import *
from indicator_extensions import *
class universal_trailing_manager_strategy(Strategy):
"""Simplified universal trailing manager: time-based entries with SL/TP/trailing management."""
def __init__(self):
super(universal_trailing_manager_strategy, self).__init__()
self._tp_points = self.Param("TakeProfitPoints", 200).SetDisplay("Take Profit", "TP in points", "Risk")
self._sl_points = self.Param("StopLossPoints", 100).SetDisplay("Stop Loss", "SL in points", "Risk")
self._trailing_points = self.Param("TrailingStopPoints", 100).SetDisplay("Trailing Stop", "Trailing distance", "Risk")
self._trailing_step = self.Param("TrailingStepPoints", 10).SetDisplay("Trailing Step", "Trailing step", "Risk")
self._time_hour = self.Param("TimeHour", 23).SetDisplay("Hour", "Scheduled hour", "Time")
self._time_minute = self.Param("TimeMinute", 59).SetDisplay("Minute", "Scheduled minute", "Time")
self._time_buy = self.Param("TimeBuy", True).SetDisplay("Time Buy", "Open buy at time", "Time")
self._time_sell = self.Param("TimeSell", True).SetDisplay("Time Sell", "Open sell at time", "Time")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(1))).SetDisplay("Candle Type", "Timeframe", "General")
@property
def CandleType(self): return self._candle_type.Value
@CandleType.setter
def CandleType(self, value): self._candle_type.Value = value
def OnReseted(self):
super(universal_trailing_manager_strategy, self).OnReseted()
self._entry_price = 0
self._stop_price = None
self._take_price = None
self._last_entry_day = -1
def OnStarted2(self, time):
super(universal_trailing_manager_strategy, self).OnStarted2(time)
self._entry_price = 0
self._stop_price = None
self._take_price = None
self._last_entry_day = -1
sub = self.SubscribeCandles(self.CandleType)
sub.Bind(self.OnProcess).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, sub)
self.DrawOwnTrades(area)
def OnProcess(self, candle):
if candle.State != CandleStates.Finished:
return
close = float(candle.ClosePrice)
high = float(candle.HighPrice)
low = float(candle.LowPrice)
sl_dist = self._sl_points.Value
tp_dist = self._tp_points.Value
trail_dist = self._trailing_points.Value
trail_step = self._trailing_step.Value
# Manage existing position
if self.Position > 0:
# Trailing
if trail_dist > 0 and self._entry_price > 0:
activation = trail_dist + trail_step
if close - self._entry_price > activation:
new_stop = close - trail_dist
if self._stop_price is None or new_stop > self._stop_price:
self._stop_price = new_stop
if self._stop_price is not None and low <= self._stop_price:
self.SellMarket()
self._reset()
return
if self._take_price is not None and high >= self._take_price:
self.SellMarket()
self._reset()
return
elif self.Position < 0:
if trail_dist > 0 and self._entry_price > 0:
activation = trail_dist + trail_step
if self._entry_price - close > activation:
new_stop = close + trail_dist
if self._stop_price is None or new_stop < self._stop_price:
self._stop_price = new_stop
if self._stop_price is not None and high >= self._stop_price:
self.BuyMarket()
self._reset()
return
if self._take_price is not None and low <= self._take_price:
self.BuyMarket()
self._reset()
return
# Time-based entries
hour = candle.CloseTime.Hour
minute = candle.CloseTime.Minute
day = candle.OpenTime.DayOfYear
if hour == self._time_hour.Value and minute == self._time_minute.Value and day != self._last_entry_day:
if self.Position == 0:
if self._time_buy.Value:
self.BuyMarket()
self._entry_price = close
self._stop_price = close - sl_dist if sl_dist > 0 else None
self._take_price = close + tp_dist if tp_dist > 0 else None
self._last_entry_day = day
elif self._time_sell.Value:
self.SellMarket()
self._entry_price = close
self._stop_price = close + sl_dist if sl_dist > 0 else None
self._take_price = close - tp_dist if tp_dist > 0 else None
self._last_entry_day = day
def _reset(self):
self._entry_price = 0
self._stop_price = None
self._take_price = None
def CreateClone(self):
return universal_trailing_manager_strategy()