Raymond Cloudy Day 策略
概述
Raymond Cloudy Day 是一套突破再入场策略,完整复现了原始 MQL5 专家 “Raymond Cloudy Day for EA” 的交易逻辑。算法通过更高周期蜡烛计算出一组关键参考价位,并在执行周期上利用这些价位寻找动量恢复信号。移植到 StockSharp 后,原有规则得到保留,同时每个模块都可以通过参数进行配置。
市场数据
- 信号蜡烛:执行交易的周期。策略订阅该序列以产生进场信号并管理头寸。
- 枢轴蜡烛:用于计算 Raymond 水平的高周期数据。默认是日线,对应 MQL5 输入参数
RayMondTimeframe。
通过 GetWorkingSecurities 自动注册上述两个订阅,策略启动时即可请求所需的数据流。
Raymond 水平的计算
每当一根高周期蜡烛收盘,策略都会按以下公式更新 Raymond 水平:
[ \beginTradeSS &= \frac{High + Low + Open + Close}{4} \ PivotRange &= High - Low \ ETB &= TradeSS + 0.382 \times PivotRange \ ETS &= TradeSS - 0.382 \times PivotRange \ TPB1 &= TradeSS + 0.618 \times PivotRange \ TPS1 &= TradeSS - 0.618 \times PivotRange \ TPB2 &= TradeSS + PivotRange \ TPS2 &= TradeSS - PivotRange \end]
最新的计算结果会保存在策略字段中,并在每次更新时写入日志,方便跟踪水平随时间的变化。
入场规则
在获得 Raymond 水平后,策略会检查每一根完成的信号蜡烛:
- 做多:若蜡烛最低价跌破
TPS1,而收盘价重新站上该水平,则开多仓。这与 EA 条件Low[1] < TPS1 && Close[1] > TPS1完全一致,旨在捕捉对支撑位的反弹。 - 做空:若整根蜡烛保持在
TPS1之上但最终收盘价跌破该水平,则开空仓(与原版相同的非对称规则)。
下单前策略会取消未成交订单,并在需要时平掉反向仓位,确保任意时刻只有一个方向的持仓。
风险控制
Raymond Cloudy Day 使用以 tick 为单位的对称保护带:
- 止损:对于多头放在入场价下方
ProtectiveOffsetTicks;对于空头放在上方相同距离。 - 止盈:与止损距离相同,但位于盈利方向。
偏移量乘以证券的 PriceStep 转换为绝对价格距离。每根信号蜡烛收盘后都会检查是否触发止损或止盈,如触发则立即平仓并重置内部保护变量。
参数
| 参数 | 说明 | 默认值 | 备注 |
|---|---|---|---|
TradeVolume |
每次进场使用的下单量。 | 1 |
启动时同步到策略的 Volume 属性。 |
ProtectiveOffsetTicks |
止损与止盈的 tick 距离。 | 500 |
通过 PriceStep 转换成价格。 |
SignalCandleType |
触发交易信号的蜡烛类型。 | 1 小时蜡烛 | 可选择任意蜡烛类型 (DataType)。 |
PivotCandleType |
计算 Raymond 水平的高周期。 | 1 天蜡烛 | 对应 MQL EA 中的 RayMondTimeframe。 |
所有参数均提供优化区间和说明,便于在 StockSharp Designer 中配置。
其他说明
- 证券必须提供
PriceStep,否则无法计算保护价格,策略会跳过进场并记录警告。 - 图表绘制包含执行周期的蜡烛以及成交的交易,需要时可自行扩展显示内容。
- 实现仅处理收盘蜡烛,不直接轮询指标数值,完全遵循
AGENTS.md中的开发规范。
原始 EA 保留的特性
- Raymond 水平的全部公式及系数(
0.382、0.618、1.0)。 - 基于第一个卖出止盈水平 (
TPS1) 的进场条件。 - 500 点对称止损/止盈,已转换为 StockSharp 环境下的 tick 偏移。
凭借这些要素,StockSharp 版本既复现了原始专家的行为,又提供了更灵活的配置与日志,便于后续研究和自动化。
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>
/// Raymond Cloudy Day strategy.
/// Computes Raymond levels from a higher timeframe and trades pullbacks around the first sell take-profit level.
/// </summary>
public class RaymondCloudyDayStrategy : Strategy
{
private readonly StrategyParam<decimal> _tradeVolume;
private readonly StrategyParam<int> _protectiveOffsetTicks;
private readonly StrategyParam<DataType> _signalCandleType;
private readonly StrategyParam<DataType> _pivotCandleType;
private decimal? _tradeSessionLevel;
private decimal? _extendedBuyLevel;
private decimal? _extendedSellLevel;
private decimal? _takeProfitBuyLevel;
private decimal? _takeProfitSellLevel;
private decimal? _takeProfitBuyLevel2;
private decimal? _takeProfitSellLevel2;
private decimal? _entryPrice;
private decimal? _takePrice;
private decimal? _stopPrice;
/// <summary>
/// Trade volume used for new positions.
/// </summary>
public decimal TradeVolume
{
get => _tradeVolume.Value;
set => _tradeVolume.Value = value;
}
/// <summary>
/// Distance in ticks used to build stop-loss and take-profit levels around the entry price.
/// </summary>
public int ProtectiveOffsetTicks
{
get => _protectiveOffsetTicks.Value;
set => _protectiveOffsetTicks.Value = value;
}
/// <summary>
/// Candle type that triggers trade signals.
/// </summary>
public DataType SignalCandleType
{
get => _signalCandleType.Value;
set => _signalCandleType.Value = value;
}
/// <summary>
/// Higher timeframe candle type used to compute Raymond levels.
/// </summary>
public DataType PivotCandleType
{
get => _pivotCandleType.Value;
set => _pivotCandleType.Value = value;
}
/// <summary>
/// Initializes strategy parameters.
/// </summary>
public RaymondCloudyDayStrategy()
{
_tradeVolume = Param(nameof(TradeVolume), 1m)
.SetGreaterThanZero()
.SetDisplay("Trade Volume", "Order volume used for entries", "Trading")
.SetOptimize(0.1m, 5m, 0.1m);
_protectiveOffsetTicks = Param(nameof(ProtectiveOffsetTicks), 500)
.SetGreaterThanZero()
.SetDisplay("Protective Offset (ticks)", "Distance in ticks for stop-loss and take-profit", "Risk Management")
.SetOptimize(50, 1000, 50);
_signalCandleType = Param(nameof(SignalCandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Signal Candle Type", "Candle type used for trade signals", "Data");
_pivotCandleType = Param(nameof(PivotCandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Pivot Candle Type", "Higher timeframe used to compute Raymond levels", "Data");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
yield return (Security, SignalCandleType);
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_tradeSessionLevel = null;
_extendedBuyLevel = null;
_extendedSellLevel = null;
_takeProfitBuyLevel = null;
_takeProfitSellLevel = null;
_takeProfitBuyLevel2 = null;
_takeProfitSellLevel2 = null;
ResetProtection();
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
Volume = TradeVolume;
var signalSubscription = SubscribeCandles(SignalCandleType);
signalSubscription
.Bind(ProcessBothCandle)
.Start();
var priceArea = CreateChartArea();
if (priceArea != null)
{
DrawCandles(priceArea, signalSubscription);
DrawOwnTrades(priceArea);
}
}
private void ProcessBothCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
ProcessPivotCandle(candle);
ProcessSignalCandle(candle);
}
private void ProcessPivotCandle(ICandleMessage candle)
{
// Skip unfinished candles to keep the level calculation consistent.
if (candle.State != CandleStates.Finished)
return;
var high = candle.HighPrice;
var low = candle.LowPrice;
var open = candle.OpenPrice;
var close = candle.ClosePrice;
var tradeSession = (high + low + open + close) / 4m;
var pivotRange = high - low;
_tradeSessionLevel = tradeSession;
_extendedBuyLevel = tradeSession + 0.382m * pivotRange;
_extendedSellLevel = tradeSession - 0.382m * pivotRange;
_takeProfitBuyLevel = tradeSession + 0.618m * pivotRange;
_takeProfitSellLevel = tradeSession - 0.618m * pivotRange;
_takeProfitBuyLevel2 = tradeSession + pivotRange;
_takeProfitSellLevel2 = tradeSession - pivotRange;
LogInfo($"Updated Raymond levels from {candle.OpenTime:u}. TradeSS={tradeSession}, ETB={_extendedBuyLevel}, ETS={_extendedSellLevel}, TPB1={_takeProfitBuyLevel}, TPS1={_takeProfitSellLevel}.");
}
private void ProcessSignalCandle(ICandleMessage candle)
{
// Manage exits first so protective logic reacts even when trading is disabled.
if (candle.State != CandleStates.Finished)
return;
ManageOpenPosition(candle);
//if (!IsFormedAndOnlineAndAllowTrading())
// return;
if (_takeProfitSellLevel is not decimal triggerLevel)
return;
var low = candle.LowPrice;
var close = candle.ClosePrice;
// Replicate the original EA condition around the TPS1 level.
if (Position <= 0 && low < triggerLevel && close > triggerLevel)
{
EnterLong(close);
}
else if (Position >= 0 && low > triggerLevel && close < triggerLevel)
{
EnterShort(close);
}
}
private void EnterLong(decimal closePrice)
{
var priceStep = Security?.PriceStep ?? 1m;
if (priceStep <= 0m)
priceStep = 1m;
CancelActiveOrders();
var volume = TradeVolume + Math.Max(0m, -Position);
BuyMarket(volume);
var offset = priceStep * ProtectiveOffsetTicks;
_entryPrice = closePrice;
_takePrice = closePrice + offset;
_stopPrice = closePrice - offset;
LogInfo($"Opened long position at {closePrice}. TP={_takePrice}, SL={_stopPrice}.");
}
private void EnterShort(decimal closePrice)
{
var priceStep = Security?.PriceStep ?? 1m;
if (priceStep <= 0m)
priceStep = 1m;
CancelActiveOrders();
var volume = TradeVolume + Math.Max(0m, Position);
SellMarket(volume);
var offset = priceStep * ProtectiveOffsetTicks;
_entryPrice = closePrice;
_takePrice = closePrice - offset;
_stopPrice = closePrice + offset;
LogInfo($"Opened short position at {closePrice}. TP={_takePrice}, SL={_stopPrice}.");
}
private void ManageOpenPosition(ICandleMessage candle)
{
if (Position == 0)
{
ResetProtection();
return;
}
if (_entryPrice is not decimal entry || _takePrice is not decimal take || _stopPrice is not decimal stop)
return;
if (Position > 0)
{
// Close the long position if price breaches the protective levels.
if (candle.LowPrice <= stop)
{
SellMarket(Position);
ResetProtection();
LogInfo($"Long stop-loss triggered at {stop}.");
return;
}
if (candle.HighPrice >= take)
{
SellMarket(Position);
ResetProtection();
LogInfo($"Long take-profit triggered at {take}.");
return;
}
}
else
{
var volume = Math.Abs(Position);
// Close the short position when stop or take-profit is hit.
if (candle.HighPrice >= stop)
{
BuyMarket(volume);
ResetProtection();
LogInfo($"Short stop-loss triggered at {stop}.");
return;
}
if (candle.LowPrice <= take)
{
BuyMarket(volume);
ResetProtection();
LogInfo($"Short take-profit triggered at {take}.");
}
}
}
private void ResetProtection()
{
_entryPrice = null;
_takePrice = null;
_stopPrice = null;
}
}
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.Strategies import Strategy
class raymond_cloudy_day_strategy(Strategy):
def __init__(self):
super(raymond_cloudy_day_strategy, self).__init__()
self._trade_volume = self.Param("TradeVolume", 1.0)
self._protective_offset_ticks = self.Param("ProtectiveOffsetTicks", 500)
self._signal_candle_type = self.Param("SignalCandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5)))
self._pivot_candle_type = self.Param("PivotCandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5)))
self._trade_session_level = None
self._extended_buy_level = None
self._extended_sell_level = None
self._take_profit_buy_level = None
self._take_profit_sell_level = None
self._take_profit_buy_level2 = None
self._take_profit_sell_level2 = None
self._entry_price = None
self._take_price = None
self._stop_price = None
@property
def TradeVolume(self):
return self._trade_volume.Value
@TradeVolume.setter
def TradeVolume(self, value):
self._trade_volume.Value = value
@property
def ProtectiveOffsetTicks(self):
return self._protective_offset_ticks.Value
@ProtectiveOffsetTicks.setter
def ProtectiveOffsetTicks(self, value):
self._protective_offset_ticks.Value = value
@property
def SignalCandleType(self):
return self._signal_candle_type.Value
@SignalCandleType.setter
def SignalCandleType(self, value):
self._signal_candle_type.Value = value
@property
def PivotCandleType(self):
return self._pivot_candle_type.Value
@PivotCandleType.setter
def PivotCandleType(self, value):
self._pivot_candle_type.Value = value
def OnReseted(self):
super(raymond_cloudy_day_strategy, self).OnReseted()
self._trade_session_level = None
self._extended_buy_level = None
self._extended_sell_level = None
self._take_profit_buy_level = None
self._take_profit_sell_level = None
self._take_profit_buy_level2 = None
self._take_profit_sell_level2 = None
self._reset_protection()
def _reset_protection(self):
self._entry_price = None
self._take_price = None
self._stop_price = None
def OnStarted2(self, time):
super(raymond_cloudy_day_strategy, self).OnStarted2(time)
self.Volume = float(self.TradeVolume)
subscription = self.SubscribeCandles(self.SignalCandleType)
subscription.Bind(self._process_both_candle).Start()
def _process_both_candle(self, candle):
if candle.State != CandleStates.Finished:
return
self._process_pivot_candle(candle)
self._process_signal_candle(candle)
def _process_pivot_candle(self, candle):
if candle.State != CandleStates.Finished:
return
high = float(candle.HighPrice)
low = float(candle.LowPrice)
open_p = float(candle.OpenPrice)
close = float(candle.ClosePrice)
trade_session = (high + low + open_p + close) / 4.0
pivot_range = high - low
self._trade_session_level = trade_session
self._extended_buy_level = trade_session + 0.382 * pivot_range
self._extended_sell_level = trade_session - 0.382 * pivot_range
self._take_profit_buy_level = trade_session + 0.618 * pivot_range
self._take_profit_sell_level = trade_session - 0.618 * pivot_range
self._take_profit_buy_level2 = trade_session + pivot_range
self._take_profit_sell_level2 = trade_session - pivot_range
def _process_signal_candle(self, candle):
if candle.State != CandleStates.Finished:
return
self._manage_open_position(candle)
if self._take_profit_sell_level is None:
return
trigger_level = self._take_profit_sell_level
low = float(candle.LowPrice)
close = float(candle.ClosePrice)
if self.Position <= 0 and low < trigger_level and close > trigger_level:
self._enter_long(close)
elif self.Position >= 0 and low > trigger_level and close < trigger_level:
self._enter_short(close)
def _enter_long(self, close_price):
price_step = 1.0
if self.Security is not None and float(self.Security.PriceStep or 0) > 0:
price_step = float(self.Security.PriceStep)
volume = float(self.TradeVolume) + max(0.0, -float(self.Position))
self.BuyMarket(volume)
offset = price_step * float(self.ProtectiveOffsetTicks)
self._entry_price = close_price
self._take_price = close_price + offset
self._stop_price = close_price - offset
def _enter_short(self, close_price):
price_step = 1.0
if self.Security is not None and float(self.Security.PriceStep or 0) > 0:
price_step = float(self.Security.PriceStep)
volume = float(self.TradeVolume) + max(0.0, float(self.Position))
self.SellMarket(volume)
offset = price_step * float(self.ProtectiveOffsetTicks)
self._entry_price = close_price
self._take_price = close_price - offset
self._stop_price = close_price + offset
def _manage_open_position(self, candle):
if self.Position == 0:
self._reset_protection()
return
if self._entry_price is None or self._take_price is None or self._stop_price is None:
return
entry = self._entry_price
take = self._take_price
stop = self._stop_price
if self.Position > 0:
if float(candle.LowPrice) <= stop:
self.SellMarket(float(self.Position))
self._reset_protection()
return
if float(candle.HighPrice) >= take:
self.SellMarket(float(self.Position))
self._reset_protection()
return
else:
volume = abs(float(self.Position))
if float(candle.HighPrice) >= stop:
self.BuyMarket(volume)
self._reset_protection()
return
if float(candle.LowPrice) <= take:
self.BuyMarket(volume)
self._reset_protection()
def CreateClone(self):
return raymond_cloudy_day_strategy()