在 GitHub 上查看
4026 – Pivots 策略
概述
该策略将 MQL/8550 中的 MetaTrader 4 资源(Pivots 指标以及 Pivots_test 专家顾问)迁移到 StockSharp 的高级 Strategy API。实现保持原始逻辑:每日计算经典枢轴点、在中心枢轴价位维持一对相反方向的挂单,并为每笔成交附加固定的止损、止盈及移动止损。
枢轴点计算
- 订阅可配置的枢轴时间框架
PivotCandleType(默认使用日线)。
- 每当该时间框架的蜡烛收盘时,根据上一交易日的 OHLC 计算经典 floor-pivot:
Pivot = (High + Low + Close) / 3
R1 = 2 × Pivot − Low
S1 = 2 × Pivot − High
R2 = Pivot + (High − Low),S2 = Pivot − (High − Low)
R3 = 2 × Pivot + High − 2 × Low,S3 = 2 × Pivot − (2 × High − Low)
- 新的水平在下一交易日生效,并通过
AddInfoLog 输出(示例:Pivot levels for 2024-04-05: P=1.0924, R1=1.0956, …)。
挂单流程
枢轴水平激活后,策略持续确保存在两张挂单:
- Buy Limit(价格
Pivot),成交后在 S2 放置 SellStop(止损)并在 R2 放置 SellLimit(止盈)。
- Sell Stop(价格
Pivot),成交后在 R2 放置 BuyStop 并在 S2 放置 BuyLimit。
所有订单均通过高级方法 BuyLimit、SellStop、SellLimit、BuyStop 注册。挂单成交后会重新计算该方向的平均成本,撤销旧的保护单,并用新的止损/止盈覆盖全部仓位量——与原始 MT4 逻辑一致(所有仓位共享 S2/R2)。当止损或止盈执行时,相应引用会自动清理。
StockSharp 采用净头寸模型,因此相反方向的成交会互相抵消(区别于 MT4 的逐笔对冲)。这是与原始专家的唯一区别。
移动止损
TrailingStopPoints 指定点数距离(会乘以 PriceStep)。
- 多头:当价格超过平均成本一定点数后,
SellStop 被上移以锁定利润。
- 空头:执行对称逻辑,下移
BuyStop。
- 更新频率由
CandleType 指定的日内时间框架决定(默认 15 分钟)。
参数
| 参数 |
说明 |
默认值 |
OrderVolume |
每张挂单的成交量。 |
0.1 |
TrailingStopPoints |
移动止损点数(为 0 时禁用)。 |
30 |
CandleType |
进行移动止损及会话管理的日内蜡烛类型。 |
15 分钟 |
PivotCandleType |
计算枢轴点使用的时间框架。 |
1 日 |
LogPivotUpdates |
是否在日志中输出枢轴点更新。 |
true |
所有数值参数均通过 StrategyParam<T> 暴露,方便在 StockSharp 中进行优化。
日志与诊断
- 枢轴点更新通过
AddInfoLog 记录,代替 MT4 中的文本标签或提示。
- 策略完全依赖 StockSharp 的高级下单与仓位管理辅助函数,没有直接操作底层订单或自建指标缓冲区。
使用提示
- 将策略连接到能够提供日线与日内蜡烛的行情源。
- 如有需要调整
PriceStep,系统会自动读取,兜底值为 0.0001。
- 可根据需要修改
OrderVolume、TrailingStopPoints 以及时间框架,以复现原策略参数。
按照要求,此版本暂不提供 Python 实现。
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>
/// Strategy that calculates classic floor pivot levels from daily candles and trades
/// breakouts around the central pivot. Goes long on close above pivot, short on close below.
/// Uses S2/R2 as stop/target levels.
/// </summary>
public class PivotsStrategy : Strategy
{
private readonly StrategyParam<DataType> _candleType;
private decimal _pivotLevel;
private decimal _r1, _r2, _s1, _s2;
private decimal? _previousClose;
private decimal? _entryPrice;
private bool _pivotReady;
private readonly List<decimal> _dailyHighs = new();
private readonly List<decimal> _dailyLows = new();
private readonly List<decimal> _dailyCloses = new();
private DateTime _currentDay;
private decimal _dayHigh;
private decimal _dayLow;
private decimal _dayClose;
public PivotsStrategy()
{
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
.SetDisplay("Candle Type", "Timeframe for signal generation", "General");
}
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
protected override void OnReseted()
{
base.OnReseted();
_pivotLevel = 0m;
_r1 = _r2 = _s1 = _s2 = 0m;
_previousClose = null;
_entryPrice = null;
_pivotReady = false;
_dailyHighs.Clear();
_dailyLows.Clear();
_dailyCloses.Clear();
_currentDay = DateTime.MinValue;
_dayHigh = 0m;
_dayLow = decimal.MaxValue;
_dayClose = 0m;
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
var sma = new SimpleMovingAverage { Length = 2 };
var subscription = SubscribeCandles(CandleType);
subscription.Bind(sma, ProcessCandle).Start();
}
private void ProcessCandle(ICandleMessage candle, decimal smaValue)
{
if (candle.State != CandleStates.Finished)
return;
var candleDay = candle.OpenTime.Date;
// Track daily OHLC
if (candleDay != _currentDay)
{
if (_currentDay != DateTime.MinValue && _dayHigh > 0m)
{
// Previous day completed, calculate pivots
var high = _dayHigh;
var low = _dayLow;
var close = _dayClose;
_pivotLevel = (high + low + close) / 3m;
_r1 = 2m * _pivotLevel - low;
_s1 = 2m * _pivotLevel - high;
_r2 = _pivotLevel + (high - low);
_s2 = _pivotLevel - (high - low);
_pivotReady = true;
}
_currentDay = candleDay;
_dayHigh = candle.HighPrice;
_dayLow = candle.LowPrice;
_dayClose = candle.ClosePrice;
}
else
{
if (candle.HighPrice > _dayHigh) _dayHigh = candle.HighPrice;
if (candle.LowPrice < _dayLow) _dayLow = candle.LowPrice;
_dayClose = candle.ClosePrice;
}
if (!_pivotReady)
{
_previousClose = candle.ClosePrice;
return;
}
if (_previousClose is null)
{
_previousClose = candle.ClosePrice;
return;
}
// Manage open positions
if (Position > 0)
{
// Exit long at R2 (take profit) or S1 (stop loss)
if (candle.HighPrice >= _r2 || candle.LowPrice <= _s1)
{
SellMarket(Math.Abs(Position));
_entryPrice = null;
}
}
else if (Position < 0)
{
// Exit short at S2 (take profit) or R1 (stop loss)
if (candle.LowPrice <= _s2 || candle.HighPrice >= _r1)
{
BuyMarket(Math.Abs(Position));
_entryPrice = null;
}
}
// Entry signals based on pivot cross
if (Position == 0)
{
var crossAbovePivot = _previousClose.Value <= _pivotLevel && candle.ClosePrice > _pivotLevel;
var crossBelowPivot = _previousClose.Value >= _pivotLevel && candle.ClosePrice < _pivotLevel;
if (crossAbovePivot)
{
BuyMarket();
_entryPrice = candle.ClosePrice;
}
else if (crossBelowPivot)
{
SellMarket();
_entryPrice = candle.ClosePrice;
}
}
_previousClose = candle.ClosePrice;
}
}
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, DateTime
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Strategies import Strategy
from StockSharp.Algo.Indicators import SimpleMovingAverage
class pivots_strategy(Strategy):
def __init__(self):
super(pivots_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(1))) \
.SetDisplay("Candle Type", "Timeframe for signal generation", "General")
self._pivot_level = 0.0
self._r1 = 0.0
self._r2 = 0.0
self._s1 = 0.0
self._s2 = 0.0
self._previous_close = None
self._entry_price = None
self._pivot_ready = False
self._current_day = None
self._day_high = 0.0
self._day_low = float('inf')
self._day_close = 0.0
@property
def CandleType(self):
return self._candle_type.Value
def OnStarted2(self, time):
super(pivots_strategy, self).OnStarted2(time)
self._sma = SimpleMovingAverage()
self._sma.Length = 2
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self._sma, self.ProcessCandle).Start()
def ProcessCandle(self, candle, sma_value):
if candle.State != CandleStates.Finished:
return
candle_day = candle.OpenTime.Date
high = float(candle.HighPrice)
low = float(candle.LowPrice)
close = float(candle.ClosePrice)
if self._current_day is None or candle_day != self._current_day:
if self._current_day is not None and self._day_high > 0:
h = self._day_high
l = self._day_low
c = self._day_close
self._pivot_level = (h + l + c) / 3.0
self._r1 = 2.0 * self._pivot_level - l
self._s1 = 2.0 * self._pivot_level - h
self._r2 = self._pivot_level + (h - l)
self._s2 = self._pivot_level - (h - l)
self._pivot_ready = True
self._current_day = candle_day
self._day_high = high
self._day_low = low
self._day_close = close
else:
if high > self._day_high:
self._day_high = high
if low < self._day_low:
self._day_low = low
self._day_close = close
if not self._pivot_ready:
self._previous_close = close
return
if self._previous_close is None:
self._previous_close = close
return
if self.Position > 0:
if high >= self._r2 or low <= self._s1:
self.SellMarket(Math.Abs(self.Position))
self._entry_price = None
elif self.Position < 0:
if low <= self._s2 or high >= self._r1:
self.BuyMarket(Math.Abs(self.Position))
self._entry_price = None
if self.Position == 0:
cross_above_pivot = self._previous_close <= self._pivot_level and close > self._pivot_level
cross_below_pivot = self._previous_close >= self._pivot_level and close < self._pivot_level
if cross_above_pivot:
self.BuyMarket()
self._entry_price = close
elif cross_below_pivot:
self.SellMarket()
self._entry_price = close
self._previous_close = close
def OnReseted(self):
super(pivots_strategy, self).OnReseted()
self._pivot_level = 0.0
self._r1 = 0.0
self._r2 = 0.0
self._s1 = 0.0
self._s2 = 0.0
self._previous_close = None
self._entry_price = None
self._pivot_ready = False
self._current_day = None
self._day_high = 0.0
self._day_low = float('inf')
self._day_close = 0.0
def CreateClone(self):
return pivots_strategy()