DVD 100-50 Cent 策略
概述
DVD 100-50 cent 是一个从 MT4 专家顾问移植而来的逆势限价策略。算法同时观察 M1、M30、H1、D1 四个周期,对潜在交易机会打分,然后在最近的“100 价位”附近挂出买入或卖出限价单。当挂单成交后,策略会按照预先计算好的止损与止盈水平管理仓位。
指标与数据
- RAVI(Range Action Verification Index):在 H1 和 D1 周期使用 SMA(2) 与 SMA(24) 对开盘价计算得到。
- 原始 K 线数据:在 M1、M30、H1 周期用于检测瞬时尖峰、盘整区间以及短期动量特征。
- 价格栅格:将最新价格四舍五入到两个小数位,并用
PointFromLevelGoPips(0.1 点单位)偏移以构建“100 价位”网格。
入场逻辑
- 根据最新的 M1 收盘价,取两位小数并加上
PointFromLevelGoPips(默认 50 ⇒ 5 点)得到目标 100 价位。 - 初始化内部评分 BAL=0,然后根据下列条件累加或扣减分数:
- 趋势过滤:当 H1 RAVI 小于 0(做多)或大于 0(做空)时加 10 分。
- 小时级尖峰确认:若前两根 H1 K 线高点/低点突破网格
RiseFilterPips,加 7 分。 - 结构确认:若当前 M1 收盘重新站回目标价位,且最近三根 H1 的低点/高点一直位于安全缓冲区(
PointFromLevelGoPips ± 30 * 0.1 点)之外,加 45 分。 - 波动性保护:若近期 M1 的高点/低点超过
HighLevelPips(默认 600 ⇒ 60 点),或在 D1 RAVI 强趋势背景下出现快速动量爆发,则减 50 分。 - 突破过滤:若过去 15 根 H1 K 线都未触及
LowLevel2Pips,减 50 分。 - 盘整过滤:若最近 8 根 M30 K 线全部位于
LowLevelPips区间内,减 50 分。
- 仅当最终得分 ≥ 50 且当前没有持仓或挂单时才会提交新的限价单。
挂单规则
- 买入限价单:在最新 M1 收盘价下方 10 点处挂单。止损位于限价价位下
StopLossPips点,止盈位于其上TakeProfitPips点。如果 D1 RAVI 在过去四天内呈现 -1 到 +5 区间的阶梯式上涨,则额外延长止盈 25 点。 - 卖出限价单:在最新 M1 收盘价上方 7 点处挂单,止损与止盈规则完全对称。当 D1 RAVI 在 -5 到 -1 区间持续下降时,同样延长止盈 25 点。
- 挂单的有效期为
OrderExpiryMinutes分钟(默认 20 分钟),超时未成交会自动撤单并清空对应的保护水平。
仓位管理
- 挂单成交后,策略会保存对应的止损/止盈价格,并在价格触及时发送市价单平仓。
- 本移植版本不启用追踪止损——原始 EA 默认关闭该功能。
- 只要存在持仓或挂单,就不会生成新的交易信号。
资金管理
- 当
UseMoneyManagement为真时,仓位规模按照 MT4 原始公式计算:以TradeSizePercent为基础,结合账户类型(迷你或标准)并限制在[0.1, MaxVolume](迷你)或[1, MaxVolume](标准)范围内。 - 关闭资金管理时,使用参数
FixedVolume指定的固定手数。 - 当账户权益低于
MarginCutoff时暂停开仓。
参数
| 参数 | 说明 | 默认值 |
|---|---|---|
AccountIsMini |
是否按迷你账户规则四舍五入仓位 | true |
UseMoneyManagement |
启用资金管理 | true |
TradeSizePercent |
每笔交易使用的权益百分比 | 10 |
FixedVolume |
关闭资金管理时的固定手数 | 0.01 |
MaxVolume |
最大可下单手数 | 4 |
StopLossPips |
止损距离(点) | 210 |
TakeProfitPips |
止盈距离(点) | 18 |
PointFromLevelGoPips |
100 价位基准偏移(0.1 点单位) | 50 |
RiseFilterPips |
H1 尖峰过滤阈值(0.1 点单位) | 700 |
HighLevelPips |
M1 尖峰过滤阈值(0.1 点单位) | 600 |
LowLevelPips |
M30 盘整区间阈值(0.1 点单位) | 250 |
LowLevel2Pips |
H1 突破确认阈值(0.1 点单位) | 450 |
MarginCutoff |
停止开仓的最低权益 | 300 |
OrderExpiryMinutes |
挂单有效期(分钟) | 20 |
使用提示
- 策略依赖四个周期的完结 K 线,请确保数据源同时提供 M1、M30、H1、D1 的同步历史数据。
- 止损和止盈通过市价单执行,以模拟 MT4 中挂单附带 SL/TP 的行为。
- 算法对点值设置较为敏感,请确认交易品种的
PriceStep与Decimals属性配置正确。
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>
/// Mean-reversion limit strategy converted from the MT4 expert advisor "DVD 100-50 cent".
/// </summary>
public class Dvd10050CentStrategy : Strategy
{
private readonly StrategyParam<bool> _accountIsMini;
private readonly StrategyParam<bool> _useMoneyManagement;
private readonly StrategyParam<decimal> _tradeSizePercent;
private readonly StrategyParam<decimal> _fixedVolume;
private readonly StrategyParam<decimal> _maxVolume;
private readonly StrategyParam<decimal> _stopLossPips;
private readonly StrategyParam<decimal> _takeProfitPips;
private readonly StrategyParam<decimal> _pointFromLevelGoPips;
private readonly StrategyParam<decimal> _riseFilterPips;
private readonly StrategyParam<decimal> _highLevelPips;
private readonly StrategyParam<decimal> _lowLevelPips;
private readonly StrategyParam<decimal> _lowLevel2Pips;
private readonly StrategyParam<decimal> _marginCutoff;
private readonly StrategyParam<int> _orderExpiryMinutes;
private readonly StrategyParam<int> _m1HistoryLength;
private readonly StrategyParam<int> _m30HistoryLength;
private readonly StrategyParam<int> _h1HistoryLength;
private SimpleMovingAverage _h1Fast = null!;
private SimpleMovingAverage _h1Slow = null!;
private SimpleMovingAverage _d1Fast = null!;
private SimpleMovingAverage _d1Slow = null!;
private readonly List<ICandleMessage> _m1History = new();
private readonly List<ICandleMessage> _m30History = new();
private readonly List<ICandleMessage> _h1Finished = new();
private ICandleMessage _h1Current;
private decimal? _raviH1;
private decimal? _raviD1Current;
private decimal? _raviD1Prev1;
private decimal? _raviD1Prev2;
private decimal? _raviD1Prev3;
private decimal _pipSize;
private decimal _pointValue;
private DateTimeOffset? _buyOrderExpiry;
private DateTimeOffset? _sellOrderExpiry;
private decimal? _pendingBuyStop;
private decimal? _pendingBuyTake;
private decimal? _pendingSellStop;
private decimal? _pendingSellTake;
private decimal? _longStop;
private decimal? _longTake;
private decimal? _shortStop;
private decimal? _shortTake;
private decimal _previousPosition;
/// <summary>
/// Initializes a new instance of the <see cref="Dvd10050CentStrategy"/> class.
/// </summary>
public Dvd10050CentStrategy()
{
_accountIsMini = Param(nameof(AccountIsMini), true)
.SetDisplay("Mini Account", "Use mini account position sizing", "Risk");
_useMoneyManagement = Param(nameof(UseMoneyManagement), true)
.SetDisplay("Use Money Management", "Enable adaptive lot sizing", "Risk");
_tradeSizePercent = Param(nameof(TradeSizePercent), 10m)
.SetDisplay("Risk Percent", "Percent of equity allocated per trade", "Risk")
.SetRange(0m, 100m)
;
_fixedVolume = Param(nameof(FixedVolume), 0.01m)
.SetDisplay("Fixed Volume", "Volume used when money management is disabled", "Risk")
.SetRange(0.01m, 100m)
;
_maxVolume = Param(nameof(MaxVolume), 4m)
.SetDisplay("Max Volume", "Ceiling for calculated trade volume", "Risk")
.SetRange(0.01m, 100m)
;
_stopLossPips = Param(nameof(StopLossPips), 210m)
.SetDisplay("Stop Loss (pips)", "Protective stop distance", "Orders")
.SetRange(0m, 1000m)
;
_takeProfitPips = Param(nameof(TakeProfitPips), 18m)
.SetDisplay("Take Profit (pips)", "Initial profit target distance", "Orders")
.SetRange(0m, 500m)
;
_pointFromLevelGoPips = Param(nameof(PointFromLevelGoPips), 50m)
.SetDisplay("Base Offset (0.1 pips)", "Offset used to build the 100 level grid", "Filters")
.SetRange(0m, 1000m)
;
_riseFilterPips = Param(nameof(RiseFilterPips), 700m)
.SetDisplay("Rise Filter (0.1 pips)", "Distance for hourly spike confirmation", "Filters")
.SetRange(0m, 5000m)
;
_highLevelPips = Param(nameof(HighLevelPips), 600m)
.SetDisplay("High Level (0.1 pips)", "One-minute spike rejection threshold", "Filters")
.SetRange(0m, 5000m)
;
_lowLevelPips = Param(nameof(LowLevelPips), 250m)
.SetDisplay("Low Level (0.1 pips)", "Half-hour consolidation ceiling", "Filters")
.SetRange(0m, 5000m)
;
_lowLevel2Pips = Param(nameof(LowLevel2Pips), 450m)
.SetDisplay("Low Level 2 (0.1 pips)", "Hourly breakout confirmation threshold", "Filters")
.SetRange(0m, 5000m)
;
_marginCutoff = Param(nameof(MarginCutoff), 300m)
.SetDisplay("Margin Cutoff", "Stop trading when equity falls below this level", "Risk")
.SetRange(0m, 1_000_000m)
;
_orderExpiryMinutes = Param(nameof(OrderExpiryMinutes), 20)
.SetDisplay("Order Expiry (minutes)", "Lifetime of pending limit orders", "Orders")
.SetRange(1, 240)
;
_m1HistoryLength = Param(nameof(M1HistoryLength), 64)
.SetDisplay("M1 History Length", "Number of M1 candles retained for analysis", "History")
.SetRange(1, 500);
_m30HistoryLength = Param(nameof(M30HistoryLength), 16)
.SetDisplay("M30 History Length", "Number of M30 candles retained for analysis", "History")
.SetRange(1, 200);
_h1HistoryLength = Param(nameof(H1HistoryLength), 16)
.SetDisplay("H1 History Length", "Number of H1 candles retained for analysis", "History")
.SetRange(1, 200);
}
/// <summary>
/// Gets or sets whether the account uses mini lot sizing.
/// </summary>
public bool AccountIsMini
{
get => _accountIsMini.Value;
set => _accountIsMini.Value = value;
}
/// <summary>
/// Gets or sets whether money management is enabled.
/// </summary>
public bool UseMoneyManagement
{
get => _useMoneyManagement.Value;
set => _useMoneyManagement.Value = value;
}
/// <summary>
/// Gets or sets the risk allocation per trade when money management is enabled.
/// </summary>
public decimal TradeSizePercent
{
get => _tradeSizePercent.Value;
set => _tradeSizePercent.Value = value;
}
/// <summary>
/// Gets or sets the fixed trade volume used when money management is disabled.
/// </summary>
public decimal FixedVolume
{
get => _fixedVolume.Value;
set => _fixedVolume.Value = value;
}
/// <summary>
/// Gets or sets the maximum volume allowed per trade.
/// </summary>
public decimal MaxVolume
{
get => _maxVolume.Value;
set => _maxVolume.Value = value;
}
/// <summary>
/// Gets or sets the stop loss distance in pips.
/// </summary>
public decimal StopLossPips
{
get => _stopLossPips.Value;
set => _stopLossPips.Value = value;
}
/// <summary>
/// Gets or sets the take profit distance in pips.
/// </summary>
public decimal TakeProfitPips
{
get => _takeProfitPips.Value;
set => _takeProfitPips.Value = value;
}
/// <summary>
/// Gets or sets the base offset that defines the 100 level grid in 0.1 pip units.
/// </summary>
public decimal PointFromLevelGoPips
{
get => _pointFromLevelGoPips.Value;
set => _pointFromLevelGoPips.Value = value;
}
/// <summary>
/// Gets or sets the spike confirmation distance for hourly candles in 0.1 pip units.
/// </summary>
public decimal RiseFilterPips
{
get => _riseFilterPips.Value;
set => _riseFilterPips.Value = value;
}
/// <summary>
/// Gets or sets the rejection distance for one-minute highs in 0.1 pip units.
/// </summary>
public decimal HighLevelPips
{
get => _highLevelPips.Value;
set => _highLevelPips.Value = value;
}
/// <summary>
/// Gets or sets the consolidation ceiling distance for 30-minute highs in 0.1 pip units.
/// </summary>
public decimal LowLevelPips
{
get => _lowLevelPips.Value;
set => _lowLevelPips.Value = value;
}
/// <summary>
/// Gets or sets the hourly breakout confirmation distance in 0.1 pip units.
/// </summary>
public decimal LowLevel2Pips
{
get => _lowLevel2Pips.Value;
set => _lowLevel2Pips.Value = value;
}
/// <summary>
/// Gets or sets the equity level that disables new trades when reached.
/// </summary>
public decimal MarginCutoff
{
get => _marginCutoff.Value;
set => _marginCutoff.Value = value;
}
/// <summary>
/// Gets or sets the pending order lifetime in minutes.
/// </summary>
public int OrderExpiryMinutes
{
get => _orderExpiryMinutes.Value;
set => _orderExpiryMinutes.Value = value;
}
/// <summary>
/// Number of one-minute candles retained for intraday analysis.
/// </summary>
public int M1HistoryLength
{
get => _m1HistoryLength.Value;
set => _m1HistoryLength.Value = value;
}
/// <summary>
/// Number of thirty-minute candles retained for intraday analysis.
/// </summary>
public int M30HistoryLength
{
get => _m30HistoryLength.Value;
set => _m30HistoryLength.Value = value;
}
/// <summary>
/// Number of hourly candles retained for intraday analysis.
/// </summary>
public int H1HistoryLength
{
get => _h1HistoryLength.Value;
set => _h1HistoryLength.Value = value;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_h1Fast = new SimpleMovingAverage { Length = 2 };
_h1Slow = new SimpleMovingAverage { Length = 24 };
_d1Fast = new SimpleMovingAverage { Length = 2 };
_d1Slow = new SimpleMovingAverage { Length = 24 };
_pipSize = CalculatePipSize();
_pointValue = _pipSize / 10m;
var m1Subscription = SubscribeCandles(TimeSpan.FromMinutes(5).TimeFrame());
m1Subscription.Bind(ProcessM1).Start();
var m30Subscription = SubscribeCandles(TimeSpan.FromMinutes(30).TimeFrame());
m30Subscription.Bind(ProcessM30).Start();
var h1Subscription = SubscribeCandles(TimeSpan.FromHours(1).TimeFrame());
h1Subscription.Bind(ProcessH1).Start();
var d1Subscription = SubscribeCandles(TimeSpan.FromMinutes(5).TimeFrame());
d1Subscription.Bind(ProcessD1).Start();
StartProtection(
takeProfit: new Unit(2, UnitTypes.Percent),
stopLoss: new Unit(1, UnitTypes.Percent)
);
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_h1Fast = null!;
_h1Slow = null!;
_d1Fast = null!;
_d1Slow = null!;
_m1History.Clear();
_m30History.Clear();
_h1Finished.Clear();
_h1Current = null;
_raviH1 = null;
_raviD1Current = null;
_raviD1Prev1 = null;
_raviD1Prev2 = null;
_raviD1Prev3 = null;
_pipSize = 0m;
_pointValue = 0m;
_buyOrderExpiry = null;
_sellOrderExpiry = null;
_pendingBuyStop = null;
_pendingBuyTake = null;
_pendingSellStop = null;
_pendingSellTake = null;
_longStop = null;
_longTake = null;
_shortStop = null;
_shortTake = null;
_previousPosition = 0m;
}
private void ProcessM1(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
_m1History.Add(candle);
TrimHistory(_m1History, M1HistoryLength);
HandlePositionState(candle);
ManageOrderExpirations(candle.CloseTime);
ManageActivePosition(candle);
if (!_h1Fast.IsFormed || !_h1Slow.IsFormed)
return;
if (HasExposure())
return;
if (!HasSufficientMargin())
return;
var orderPlaced = false;
if (TryCalculateBuyScore(candle, out var buyLevel, out var buyScore) && buyScore >= 0m)
{
orderPlaced = PlaceBuyLimit(candle);
}
if (!orderPlaced && TryCalculateSellScore(candle, out var sellLevel, out var sellScore) && sellScore >= 0m)
{
PlaceSellLimit(candle);
}
}
private void ProcessM30(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
_m30History.Add(candle);
TrimHistory(_m30History, M30HistoryLength);
}
private void ProcessH1(ICandleMessage candle)
{
_h1Current = candle;
if (candle.State != CandleStates.Finished)
return;
_h1Finished.Add(candle);
TrimHistory(_h1Finished, H1HistoryLength);
_h1Fast.Process(new DecimalIndicatorValue(_h1Fast, candle.OpenPrice, candle.CloseTime) { IsFinal = true });
_h1Slow.Process(new DecimalIndicatorValue(_h1Slow, candle.OpenPrice, candle.CloseTime) { IsFinal = true });
if (!_h1Fast.IsFormed || !_h1Slow.IsFormed)
return;
var slow = _h1Slow.GetCurrentValue();
if (slow == 0m)
return;
var fast = _h1Fast.GetCurrentValue();
_raviH1 = 100m * (fast - slow) / slow;
}
private void ProcessD1(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
_d1Fast.Process(new DecimalIndicatorValue(_d1Fast, candle.OpenPrice, candle.CloseTime) { IsFinal = true });
_d1Slow.Process(new DecimalIndicatorValue(_d1Slow, candle.OpenPrice, candle.CloseTime) { IsFinal = true });
if (!_d1Fast.IsFormed || !_d1Slow.IsFormed)
return;
var slow = _d1Slow.GetCurrentValue();
if (slow == 0m)
return;
var fast = _d1Fast.GetCurrentValue();
var ravi = 100m * (fast - slow) / slow;
_raviD1Prev3 = _raviD1Prev2;
_raviD1Prev2 = _raviD1Prev1;
_raviD1Prev1 = _raviD1Current;
_raviD1Current = ravi;
}
private void HandlePositionState(ICandleMessage candle)
{
var currentPosition = Position;
if (currentPosition > 0m && _previousPosition <= 0m)
{
_longStop = _pendingBuyStop;
_longTake = _pendingBuyTake;
_pendingBuyStop = null;
_pendingBuyTake = null;
_buyOrderExpiry = null;
}
else if (currentPosition < 0m && _previousPosition >= 0m)
{
_shortStop = _pendingSellStop;
_shortTake = _pendingSellTake;
_pendingSellStop = null;
_pendingSellTake = null;
_sellOrderExpiry = null;
}
else if (currentPosition == 0m && _previousPosition != 0m)
{
ResetTradeLevels();
}
_previousPosition = currentPosition;
}
private void ManageOrderExpirations(DateTimeOffset currentTime)
{
if (_buyOrderExpiry is DateTimeOffset buyExpiry)
{
if (!HasActiveLimitOrder(Sides.Buy))
{
_buyOrderExpiry = null;
}
else if (currentTime >= buyExpiry)
{
CancelSideOrders(Sides.Buy);
_buyOrderExpiry = null;
_pendingBuyStop = null;
_pendingBuyTake = null;
}
}
if (_sellOrderExpiry is DateTimeOffset sellExpiry)
{
if (!HasActiveLimitOrder(Sides.Sell))
{
_sellOrderExpiry = null;
}
else if (currentTime >= sellExpiry)
{
CancelSideOrders(Sides.Sell);
_sellOrderExpiry = null;
_pendingSellStop = null;
_pendingSellTake = null;
}
}
}
private void ManageActivePosition(ICandleMessage candle)
{
if (Position > 0m)
{
if (_longStop is decimal stop && candle.LowPrice <= stop)
{
SellMarket(Math.Abs(Position));
ResetTradeLevels();
return;
}
if (_longTake is decimal take && candle.HighPrice >= take)
{
SellMarket(Math.Abs(Position));
ResetTradeLevels();
}
}
else if (Position < 0m)
{
if (_shortStop is decimal stop && candle.HighPrice >= stop)
{
BuyMarket(Math.Abs(Position));
ResetTradeLevels();
return;
}
if (_shortTake is decimal take && candle.LowPrice <= take)
{
BuyMarket(Math.Abs(Position));
ResetTradeLevels();
}
}
}
private bool TryCalculateBuyScore(ICandleMessage candle, out decimal level100, out decimal score)
{
score = 0m;
level100 = 0m;
if (_raviH1 is not decimal raviH1 || _raviD1Current is not decimal raviD1)
return false;
var previousM1 = GetM1Candle(1);
var h1Low0 = GetH1Low(0);
var h1Low1 = GetH1Low(1);
var h1Low2 = GetH1Low(2);
var h1High1 = GetH1High(1);
var h1High2 = GetH1High(2);
if (previousM1 is null || h1Low0 is null || h1Low1 is null || h1Low2 is null || h1High1 is null || h1High2 is null)
return false;
level100 = Math.Round(candle.ClosePrice, 2, MidpointRounding.AwayFromZero) + PointFromLevelGoPips * _pointValue;
var riseThreshold = level100 + RiseFilterPips * _pointValue;
var baseLow = level100 - PointFromLevelGoPips * _pointValue;
var tolerance = 30m * _pointValue;
if (raviH1 < 0m)
score += 10m;
if (h1High1 > riseThreshold || h1High2 > riseThreshold)
score += 7m;
if (candle.ClosePrice < level100 && previousM1.ClosePrice > level100 &&
h1Low0.Value > baseLow + tolerance && h1Low1.Value > baseLow + tolerance && h1Low2.Value > baseLow)
{
score += 45m;
}
if (CheckM1HighAbove(level100 + HighLevelPips * _pointValue, 12))
score -= 50m;
if (raviD1 < -2m && CheckM1ImpulseForBuy())
score -= 50m;
if (!CheckH1BreakAbove(level100 + LowLevel2Pips * _pointValue))
score -= 50m;
if (CheckM30CompressionAbove(level100 + LowLevelPips * _pointValue))
score -= 50m;
return true;
}
private bool TryCalculateSellScore(ICandleMessage candle, out decimal level100, out decimal score)
{
score = 0m;
level100 = 0m;
if (_raviH1 is not decimal raviH1 || _raviD1Current is not decimal raviD1)
return false;
var previousM1 = GetM1Candle(1);
var h1High0 = GetH1High(0);
var h1High1 = GetH1High(1);
var h1High2 = GetH1High(2);
var h1Low1 = GetH1Low(1);
var h1Low2 = GetH1Low(2);
if (previousM1 is null || h1High0 is null || h1High1 is null || h1High2 is null || h1Low1 is null || h1Low2 is null)
return false;
level100 = Math.Round(candle.ClosePrice, 2, MidpointRounding.AwayFromZero) - PointFromLevelGoPips * _pointValue;
var fallThreshold = level100 - RiseFilterPips * _pointValue;
var baseHigh = level100 + PointFromLevelGoPips * _pointValue;
var tolerance = 30m * _pointValue;
if (raviH1 > 0m)
score += 10m;
if (h1Low1 < fallThreshold || h1Low2 < fallThreshold)
score += 7m;
if (candle.ClosePrice > level100 && previousM1.ClosePrice < level100 &&
h1High0.Value < baseHigh - tolerance && h1High1.Value < baseHigh - tolerance && h1High2.Value < baseHigh)
{
score += 45m;
}
if (CheckM1LowBelow(level100 - HighLevelPips * _pointValue, 12))
score -= 50m;
if (raviD1 > 2m && CheckM1ImpulseForSell())
score -= 50m;
if (!CheckH1BreakBelow(level100 - LowLevel2Pips * _pointValue))
score -= 50m;
if (CheckM30CompressionBelow(level100 - LowLevelPips * _pointValue))
score -= 50m;
return true;
}
private bool PlaceBuyLimit(ICandleMessage candle)
{
var volume = CalculateOrderVolume();
if (volume <= 0m)
return false;
var entryPrice = Math.Max(candle.ClosePrice - 10m * _pipSize, 0m);
var stopPrice = entryPrice - StopLossPips * _pipSize;
var takePrice = entryPrice + TakeProfitPips * _pipSize;
if (_raviD1Current is decimal ravi && _raviD1Prev1 is decimal prev1 && _raviD1Prev2 is decimal prev2 && _raviD1Prev3 is decimal prev3)
{
if (ravi > 1m && ravi < 5m && prev1 < ravi && prev2 < prev1 && prev3 < prev2)
{
takePrice += 25m * _pipSize;
}
}
BuyLimit(price: entryPrice, volume: volume);
_buyOrderExpiry = candle.CloseTime + TimeSpan.FromMinutes(OrderExpiryMinutes);
_pendingBuyStop = stopPrice;
_pendingBuyTake = takePrice;
return true;
}
private bool PlaceSellLimit(ICandleMessage candle)
{
var volume = CalculateOrderVolume();
if (volume <= 0m)
return false;
var entryPrice = candle.ClosePrice + 7m * _pipSize;
var stopPrice = entryPrice + StopLossPips * _pipSize;
var takePrice = entryPrice - TakeProfitPips * _pipSize;
if (_raviD1Current is decimal ravi && _raviD1Prev1 is decimal prev1 && _raviD1Prev2 is decimal prev2 && _raviD1Prev3 is decimal prev3)
{
if (ravi < -1m && ravi > -5m && prev1 > ravi && prev2 > prev1 && prev3 > prev2)
{
takePrice -= 25m * _pipSize;
}
}
SellLimit(price: entryPrice, volume: volume);
_sellOrderExpiry = candle.CloseTime + TimeSpan.FromMinutes(OrderExpiryMinutes);
_pendingSellStop = stopPrice;
_pendingSellTake = takePrice;
return true;
}
private bool CheckM1HighAbove(decimal threshold, int candles)
{
for (var i = 0; i < candles; i++)
{
var candle = GetM1Candle(i);
if (candle is null)
break;
if (candle.HighPrice > threshold)
return true;
}
return false;
}
private bool CheckM1LowBelow(decimal threshold, int candles)
{
for (var i = 0; i < candles; i++)
{
var candle = GetM1Candle(i);
if (candle is null)
break;
if (candle.LowPrice < threshold)
return true;
}
return false;
}
private bool CheckM1ImpulseForBuy()
{
for (var shift = 0; shift <= 30; shift++)
{
var current = GetM1Candle(shift);
var future = GetM1Candle(shift + 3);
if (current is null || future is null)
break;
if (future.HighPrice - current.LowPrice > 300m * _pointValue && future.OpenPrice > current.ClosePrice)
return true;
}
return false;
}
private bool CheckM1ImpulseForSell()
{
for (var shift = 0; shift <= 30; shift++)
{
var current = GetM1Candle(shift);
var future = GetM1Candle(shift + 3);
if (current is null || future is null)
break;
if (current.HighPrice - future.LowPrice > 300m * _pointValue && current.ClosePrice > future.OpenPrice)
return true;
}
return false;
}
private bool CheckH1BreakAbove(decimal threshold)
{
for (var shift = 0; shift <= 14; shift++)
{
var high = GetH1High(shift);
if (high is null)
break;
if (high.Value > threshold)
return true;
}
return false;
}
private bool CheckH1BreakBelow(decimal threshold)
{
for (var shift = 0; shift <= 14; shift++)
{
var low = GetH1Low(shift);
if (low is null)
break;
if (low.Value < threshold)
return true;
}
return false;
}
private bool CheckM30CompressionAbove(decimal threshold)
{
for (var shift = 0; shift <= 7; shift++)
{
var candle = GetM30Candle(shift);
if (candle is null)
return false;
if (candle.HighPrice >= threshold)
return false;
}
return true;
}
private bool CheckM30CompressionBelow(decimal threshold)
{
for (var shift = 0; shift <= 7; shift++)
{
var candle = GetM30Candle(shift);
if (candle is null)
return false;
if (candle.LowPrice <= threshold)
return false;
}
return true;
}
private ICandleMessage GetM1Candle(int shift)
{
var index = _m1History.Count - 1 - shift;
return index >= 0 && index < _m1History.Count ? _m1History[index] : null;
}
private ICandleMessage GetM30Candle(int shift)
{
var index = _m30History.Count - 1 - shift;
return index >= 0 && index < _m30History.Count ? _m30History[index] : null;
}
private decimal? GetH1High(int shift)
{
if (shift == 0)
return _h1Current?.HighPrice;
var index = _h1Finished.Count - shift;
return index >= 0 && index < _h1Finished.Count ? _h1Finished[index].HighPrice : null;
}
private decimal? GetH1Low(int shift)
{
if (shift == 0)
return _h1Current?.LowPrice;
var index = _h1Finished.Count - shift;
return index >= 0 && index < _h1Finished.Count ? _h1Finished[index].LowPrice : null;
}
private void CancelSideOrders(Sides side)
{
foreach (var order in Orders.Where(o => o.State is OrderStates.Active or OrderStates.Pending))
{
if (order.Type != OrderTypes.Limit || order.Side != side)
continue;
CancelOrder(order);
}
}
private bool HasActiveLimitOrder(Sides side)
{
foreach (var order in Orders.Where(o => o.State is OrderStates.Active or OrderStates.Pending))
{
if (order.Type == OrderTypes.Limit && order.Side == side)
return true;
}
return false;
}
private bool HasExposure()
{
if (Position != 0m)
return true;
foreach (var order in Orders.Where(o => o.State is OrderStates.Active or OrderStates.Pending))
{
if (order.Type == OrderTypes.Limit)
return true;
}
return false;
}
private bool HasSufficientMargin()
{
if (MarginCutoff <= 0m)
return true;
var portfolio = Portfolio;
if (portfolio is null)
return true;
var equity = portfolio.CurrentValue ?? 0m;
if (equity <= 0m)
equity = portfolio.BeginValue ?? 0m;
return equity >= MarginCutoff;
}
private decimal CalculateOrderVolume()
{
if (!UseMoneyManagement)
return FixedVolume;
var portfolio = Portfolio;
if (portfolio is null)
return FixedVolume;
var equity = portfolio.CurrentValue ?? 0m;
if (equity <= 0m)
equity = portfolio.BeginValue ?? 0m;
if (equity <= 0m)
return FixedVolume;
var lot = Math.Floor(equity * TradeSizePercent / 1000m) / 100m;
if (AccountIsMini)
{
lot = Math.Floor(lot * 100m) / 100m;
if (lot < 0.1m)
lot = 0.1m;
}
else
{
if (lot < 1m)
lot = 1m;
}
if (lot > MaxVolume)
lot = MaxVolume;
return lot;
}
private void ResetTradeLevels()
{
_pendingBuyStop = null;
_pendingBuyTake = null;
_pendingSellStop = null;
_pendingSellTake = null;
_longStop = null;
_longTake = null;
_shortStop = null;
_shortTake = null;
}
private static void TrimHistory(ICollection<ICandleMessage> history, int maxCount)
{
while (history.Count > maxCount)
{
if (history is List<ICandleMessage> list)
list.RemoveAt(0);
else
break;
}
}
private decimal CalculatePipSize()
{
var step = Security?.PriceStep ?? 0.0001m;
var decimals = Security?.Decimals ?? 0;
var adjust = decimals == 3 || decimals == 5 ? 10m : 1m;
var pip = step * adjust;
return pip == 0m ? 0.0001m : pip;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
import math
from System import TimeSpan, Decimal
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Strategies import Strategy
from StockSharp.Algo.Indicators import SimpleMovingAverage
from indicator_extensions import *
class dvd10050_cent_strategy(Strategy):
def __init__(self):
super(dvd10050_cent_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))) \
.SetDisplay("Candle Type", "Monitoring timeframe.", "General")
self._stop_loss_pips = self.Param("StopLossPips", 210.0) \
.SetDisplay("Stop Loss (pips)", "Protective stop distance.", "Orders")
self._take_profit_pips = self.Param("TakeProfitPips", 18.0) \
.SetDisplay("Take Profit (pips)", "Profit target distance.", "Orders")
self._point_offset = self.Param("PointFromLevelGoPips", 50.0) \
.SetDisplay("Base Offset (0.1 pips)", "Offset for 100 level grid.", "Filters")
self._rise_filter = self.Param("RiseFilterPips", 700.0) \
.SetDisplay("Rise Filter (0.1 pips)", "Hourly spike confirmation.", "Filters")
self._high_level = self.Param("HighLevelPips", 600.0) \
.SetDisplay("High Level (0.1 pips)", "One-minute spike rejection.", "Filters")
self._low_level = self.Param("LowLevelPips", 250.0) \
.SetDisplay("Low Level (0.1 pips)", "Half-hour consolidation ceiling.", "Filters")
self._low_level2 = self.Param("LowLevel2Pips", 450.0) \
.SetDisplay("Low Level 2 (0.1 pips)", "Hourly breakout confirmation.", "Filters")
self._m1_hist_len = self.Param("M1HistoryLength", 64) \
.SetDisplay("M1 History Length", "Number of M1 candles retained.", "History")
self._h1_hist_len = self.Param("H1HistoryLength", 16) \
.SetDisplay("H1 History Length", "Number of H1 candles retained.", "History")
self._m30_hist_len = self.Param("M30HistoryLength", 16) \
.SetDisplay("M30 History Length", "Number of M30 candles retained.", "History")
self._m1_history = []
self._m30_history = []
self._h1_finished = []
self._h1_fast = None
self._h1_slow = None
self._ravi_h1 = None
self._pip_size = 0.0001
self._point_value = 0.00001
self._entry_price = 0.0
self._long_stop = None
self._long_take = None
self._short_stop = None
self._short_take = None
@property
def CandleType(self):
return self._candle_type.Value
@property
def StopLossPips(self):
return float(self._stop_loss_pips.Value)
@property
def TakeProfitPips(self):
return float(self._take_profit_pips.Value)
@property
def PointFromLevelGoPips(self):
return float(self._point_offset.Value)
@property
def RiseFilterPips(self):
return float(self._rise_filter.Value)
@property
def HighLevelPips(self):
return float(self._high_level.Value)
@property
def LowLevelPips(self):
return float(self._low_level.Value)
@property
def LowLevel2Pips(self):
return float(self._low_level2.Value)
@property
def M1HistoryLength(self):
return self._m1_hist_len.Value
@property
def H1HistoryLength(self):
return self._h1_hist_len.Value
@property
def M30HistoryLength(self):
return self._m30_hist_len.Value
def OnStarted2(self, time):
super(dvd10050_cent_strategy, self).OnStarted2(time)
self._m1_history = []
self._m30_history = []
self._h1_finished = []
self._ravi_h1 = None
self._entry_price = 0.0
self._long_stop = None
self._long_take = None
self._short_stop = None
self._short_take = None
sec = self.Security
step = 0.0001
if sec is not None:
ps = sec.PriceStep
if ps is not None and float(ps) > 0:
step = float(ps)
self._pip_size = step
self._point_value = step / 10.0
self._h1_fast = SimpleMovingAverage()
self._h1_fast.Length = 2
self._h1_slow = SimpleMovingAverage()
self._h1_slow.Length = 24
sub_m1 = self.SubscribeCandles(self.CandleType)
sub_m1.Bind(self.ProcessM1).Start()
sub_m30 = self.SubscribeCandles(DataType.TimeFrame(TimeSpan.FromMinutes(30)))
sub_m30.Bind(self.ProcessM30).Start()
sub_h1 = self.SubscribeCandles(DataType.TimeFrame(TimeSpan.FromHours(1)))
sub_h1.Bind(self.ProcessH1).Start()
def ProcessM1(self, candle):
if candle.State != CandleStates.Finished:
return
close = float(candle.ClosePrice)
high = float(candle.HighPrice)
low = float(candle.LowPrice)
self._m1_history.append((close, high, low))
while len(self._m1_history) > self.M1HistoryLength:
self._m1_history.pop(0)
# manage position
if self.Position > 0:
if self._long_stop is not None and low <= self._long_stop:
self.SellMarket()
self._reset_levels()
return
if self._long_take is not None and high >= self._long_take:
self.SellMarket()
self._reset_levels()
return
elif self.Position < 0:
if self._short_stop is not None and high >= self._short_stop:
self.BuyMarket()
self._reset_levels()
return
if self._short_take is not None and low <= self._short_take:
self.BuyMarket()
self._reset_levels()
return
if self._ravi_h1 is None:
return
if self.Position != 0:
return
# try buy
buy_score = self._calc_buy_score(close, high, low)
if buy_score is not None and buy_score >= 0:
entry = close
sl = entry - self.StopLossPips * self._pip_size
tp = entry + self.TakeProfitPips * self._pip_size
self._entry_price = entry
self._long_stop = sl
self._long_take = tp
self._short_stop = None
self._short_take = None
self.BuyMarket()
return
# try sell
sell_score = self._calc_sell_score(close, high, low)
if sell_score is not None and sell_score >= 0:
entry = close
sl = entry + self.StopLossPips * self._pip_size
tp = entry - self.TakeProfitPips * self._pip_size
self._entry_price = entry
self._short_stop = sl
self._short_take = tp
self._long_stop = None
self._long_take = None
self.SellMarket()
def ProcessM30(self, candle):
if candle.State != CandleStates.Finished:
return
high = float(candle.HighPrice)
low = float(candle.LowPrice)
self._m30_history.append((high, low))
while len(self._m30_history) > self.M30HistoryLength:
self._m30_history.pop(0)
def ProcessH1(self, candle):
if candle.State != CandleStates.Finished:
return
high = float(candle.HighPrice)
low = float(candle.LowPrice)
op = float(candle.OpenPrice)
self._h1_finished.append((high, low, op))
while len(self._h1_finished) > self.H1HistoryLength:
self._h1_finished.pop(0)
process_float(self._h1_fast, Decimal(op), candle.CloseTime, True)
process_float(self._h1_slow, Decimal(op), candle.CloseTime, True)
if not self._h1_fast.IsFormed or not self._h1_slow.IsFormed:
return
from StockSharp.Algo.Indicators import IndicatorHelper
slow_v = float(IndicatorHelper.GetCurrentValue(self._h1_slow))
if slow_v == 0:
return
fast_v = float(IndicatorHelper.GetCurrentValue(self._h1_fast))
self._ravi_h1 = 100.0 * (fast_v - slow_v) / slow_v
def _get_m1(self, shift):
idx = len(self._m1_history) - 1 - shift
if 0 <= idx < len(self._m1_history):
return self._m1_history[idx]
return None
def _get_h1_high(self, shift):
idx = len(self._h1_finished) - 1 - shift
if 0 <= idx < len(self._h1_finished):
return self._h1_finished[idx][0]
return None
def _get_h1_low(self, shift):
idx = len(self._h1_finished) - 1 - shift
if 0 <= idx < len(self._h1_finished):
return self._h1_finished[idx][1]
return None
def _get_m30(self, shift):
idx = len(self._m30_history) - 1 - shift
if 0 <= idx < len(self._m30_history):
return self._m30_history[idx]
return None
def _check_m1_high_above(self, threshold, count):
for i in range(count):
m = self._get_m1(i)
if m is None:
break
if m[1] > threshold:
return True
return False
def _check_m1_low_below(self, threshold, count):
for i in range(count):
m = self._get_m1(i)
if m is None:
break
if m[2] < threshold:
return True
return False
def _check_h1_break_above(self, threshold):
for i in range(15):
h = self._get_h1_high(i)
if h is None:
break
if h > threshold:
return True
return False
def _check_h1_break_below(self, threshold):
for i in range(15):
lo = self._get_h1_low(i)
if lo is None:
break
if lo < threshold:
return True
return False
def _check_m30_compression_above(self, threshold):
for i in range(8):
m = self._get_m30(i)
if m is None:
return False
if m[0] >= threshold:
return False
return True
def _check_m30_compression_below(self, threshold):
for i in range(8):
m = self._get_m30(i)
if m is None:
return False
if m[1] <= threshold:
return False
return True
def _calc_buy_score(self, close, high, low):
if self._ravi_h1 is None:
return None
prev_m1 = self._get_m1(1)
h1_low0 = self._get_h1_low(0)
h1_low1 = self._get_h1_low(1)
h1_low2 = self._get_h1_low(2)
h1_high1 = self._get_h1_high(1)
h1_high2 = self._get_h1_high(2)
if prev_m1 is None or h1_low0 is None or h1_low1 is None or h1_low2 is None:
return None
if h1_high1 is None or h1_high2 is None:
return None
pv = self._point_value
level100 = round(close, 2) + self.PointFromLevelGoPips * pv
rise_thr = level100 + self.RiseFilterPips * pv
base_low = level100 - self.PointFromLevelGoPips * pv
tol = 30.0 * pv
score = 0.0
if self._ravi_h1 < 0:
score += 10.0
if h1_high1 > rise_thr or h1_high2 > rise_thr:
score += 7.0
if close < level100 and prev_m1[0] > level100 and h1_low0 > base_low + tol and h1_low1 > base_low + tol and h1_low2 > base_low:
score += 45.0
if self._check_m1_high_above(level100 + self.HighLevelPips * pv, 12):
score -= 50.0
if not self._check_h1_break_above(level100 + self.LowLevel2Pips * pv):
score -= 50.0
if self._check_m30_compression_above(level100 + self.LowLevelPips * pv):
score -= 50.0
return score
def _calc_sell_score(self, close, high, low):
if self._ravi_h1 is None:
return None
prev_m1 = self._get_m1(1)
h1_high0 = self._get_h1_high(0)
h1_high1 = self._get_h1_high(1)
h1_high2 = self._get_h1_high(2)
h1_low1 = self._get_h1_low(1)
h1_low2 = self._get_h1_low(2)
if prev_m1 is None or h1_high0 is None or h1_high1 is None or h1_high2 is None:
return None
if h1_low1 is None or h1_low2 is None:
return None
pv = self._point_value
level100 = round(close, 2) - self.PointFromLevelGoPips * pv
fall_thr = level100 - self.RiseFilterPips * pv
base_high = level100 + self.PointFromLevelGoPips * pv
tol = 30.0 * pv
score = 0.0
if self._ravi_h1 > 0:
score += 10.0
if h1_low1 < fall_thr or h1_low2 < fall_thr:
score += 7.0
if close > level100 and prev_m1[0] < level100 and h1_high0 < base_high - tol and h1_high1 < base_high - tol and h1_high2 < base_high:
score += 45.0
if self._check_m1_low_below(level100 - self.HighLevelPips * pv, 12):
score -= 50.0
if not self._check_h1_break_below(level100 - self.LowLevel2Pips * pv):
score -= 50.0
if self._check_m30_compression_below(level100 - self.LowLevelPips * pv):
score -= 50.0
return score
def _reset_levels(self):
self._long_stop = None
self._long_take = None
self._short_stop = None
self._short_take = None
self._entry_price = 0.0
def OnReseted(self):
super(dvd10050_cent_strategy, self).OnReseted()
self._m1_history = []
self._m30_history = []
self._h1_finished = []
self._ravi_h1 = None
self._entry_price = 0.0
self._long_stop = None
self._long_take = None
self._short_stop = None
self._short_take = None
def CreateClone(self):
return dvd10050_cent_strategy()