Doji Trader 策略
该策略复刻经典 Doji Trader EA 的核心思想。 它在每根 K 线收盘后检查是否出现小实体的十字星,一旦下一根 K 线收盘 突破十字星区间,就沿突破方向入场。
交易逻辑
- 只处理已经收盘的 K 线。默认使用 1 小时周期,可通过参数
CandleType调整。 - 只有当最新 K 线的收盘时间落在交易窗口
[StartHour, EndHour)内时才允许 下单,时间基于交易所时区。 - 算法保存最近三根收盘 K 线。当前 K 线与之前两根 K 线(倒数第二和第三根) 进行比较。
- 当实体高度小于
MaximumDojiHeight * pip时,该 K 线被视为十字星。pip 值由 品种的最小报价步长计算而来,若为 3 或 5 位小数报价会自动放大 10 倍。 - 若最新一根 K 线收盘价 高于 最近的十字星最高价,则建立或反手做多; 若收盘价 低于 十字星最低价,则建立或反手做空;价格保持在区间内时 不产生信号。
- 下单手数取自策略的
Volume属性。当出现反向信号时,算法会发送足够的 数量来平掉旧仓位并建立目标持仓,确保始终只有一个净持仓。
风险控制
StopLossPips与TakeProfitPips以点(pip)为单位设置止损和止盈距离, 设为 0 则关闭相应保护订单。- 启动时调用一次
StartProtection,并使用市价单退出,以复现 MQL 版本中 直接平仓再开仓的行为。
参数说明
| 名称 | 说明 | 默认值 |
|---|---|---|
CandleType |
参与计算的 K 线周期。 | 1 小时 |
StartHour |
交易窗口起始小时(含)。 | 8 |
EndHour |
交易窗口结束小时(不含)。 | 17 |
MaximumDojiHeight |
判定十字星的最大实体高度(pip)。 | 1 |
StopLossPips |
止损距离(pip)。 | 50 |
TakeProfitPips |
止盈距离(pip)。 | 50 |
额外说明
- 策略以净仓模型为前提。对于 3 或 5 位小数报价的品种,pip 大小会自动乘以 10。
- 启动前请在
Volume属性中设置期望的交易手数。 - 策略不依赖额外指标,只使用原始 K 线数据。
- 按要求目前仅提供 C# 版本,暂不包含 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;
using StockSharp.Algo;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Strategy inspired by the Doji Trader Expert Advisor.
/// Looks for a recent doji candle and trades when the next candle closes beyond the doji range.
/// </summary>
public class DojiTraderStrategy : Strategy
{
private readonly StrategyParam<decimal> _stopLossPips;
private readonly StrategyParam<decimal> _takeProfitPips;
private readonly StrategyParam<int> _startHour;
private readonly StrategyParam<int> _endHour;
private readonly StrategyParam<decimal> _maximumDojiHeight;
private readonly StrategyParam<DataType> _candleType;
private ICandleMessage _previousCandle;
private ICandleMessage _twoAgoCandle;
private ICandleMessage _threeAgoCandle;
private decimal _pipSize;
/// <summary>
/// Stop-loss distance in pips.
/// </summary>
public decimal StopLossPips
{
get => _stopLossPips.Value;
set => _stopLossPips.Value = value;
}
/// <summary>
/// Take-profit distance in pips.
/// </summary>
public decimal TakeProfitPips
{
get => _takeProfitPips.Value;
set => _takeProfitPips.Value = value;
}
/// <summary>
/// First trading hour (inclusive) using exchange time.
/// </summary>
public int StartHour
{
get => _startHour.Value;
set => _startHour.Value = value;
}
/// <summary>
/// Last trading hour (exclusive) using exchange time.
/// </summary>
public int EndHour
{
get => _endHour.Value;
set => _endHour.Value = value;
}
/// <summary>
/// Maximum body height for a candle to be considered a doji (in pips).
/// </summary>
public decimal MaximumDojiHeight
{
get => _maximumDojiHeight.Value;
set => _maximumDojiHeight.Value = value;
}
/// <summary>
/// Candle type used for pattern detection.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initializes a new instance of the <see cref="DojiTraderStrategy"/>.
/// </summary>
public DojiTraderStrategy()
{
_stopLossPips = Param(nameof(StopLossPips), 50m)
.SetDisplay("Stop Loss", "Stop-loss distance in pips", "Protection")
.SetRange(0m, 500m);
_takeProfitPips = Param(nameof(TakeProfitPips), 50m)
.SetDisplay("Take Profit", "Take-profit distance in pips", "Protection")
.SetRange(0m, 500m);
_startHour = Param(nameof(StartHour), 8)
.SetDisplay("Start Hour", "Hour when trading becomes active", "Session")
.SetRange(0, 23);
_endHour = Param(nameof(EndHour), 17)
.SetDisplay("End Hour", "Hour when trading stops (exclusive)", "Session")
.SetRange(1, 24);
_maximumDojiHeight = Param(nameof(MaximumDojiHeight), 1m)
.SetDisplay("Doji Height", "Maximum doji body height in pips", "Pattern")
.SetRange(0.1m, 20m);
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
.SetDisplay("Candle Type", "Timeframe used for doji detection", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_previousCandle = null;
_twoAgoCandle = null;
_threeAgoCandle = null;
_pipSize = 0m;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_pipSize = CalculatePipSize();
// Configure stop-loss and take-profit protection once at start.
var takeProfitUnit = TakeProfitPips > 0m ? new Unit(TakeProfitPips * _pipSize, UnitTypes.Absolute) : default;
var stopLossUnit = StopLossPips > 0m ? new Unit(StopLossPips * _pipSize, UnitTypes.Absolute) : default;
if (takeProfitUnit != default || stopLossUnit != default)
{
StartProtection(takeProfitUnit, stopLossUnit, useMarketOrders: true);
}
else
{
StartProtection(null, null);
}
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(ProcessCandle)
.Start();
}
private void ProcessCandle(ICandleMessage candle)
{
// Process only finished candles.
if (candle.State != CandleStates.Finished)
return;
// Skip trading outside the configured session window.
var nextHour = candle.CloseTime.Hour;
if (nextHour < StartHour || nextHour >= EndHour)
{
ShiftHistory(candle);
return;
}
// We need at least three completed candles for pattern detection.
if (_twoAgoCandle is null)
{
ShiftHistory(candle);
return;
}
var pipSize = _pipSize > 0m ? _pipSize : (_pipSize = CalculatePipSize());
var dojiHeight = MaximumDojiHeight * pipSize;
var dojiHigh = 0m;
var dojiLow = 0m;
// Check the two candles before the current close for the most recent doji.
if (IsDoji(_twoAgoCandle, dojiHeight))
{
dojiHigh = _twoAgoCandle.HighPrice;
dojiLow = _twoAgoCandle.LowPrice;
}
else if (_threeAgoCandle is not null && IsDoji(_threeAgoCandle, dojiHeight))
{
dojiHigh = _threeAgoCandle.HighPrice;
dojiLow = _threeAgoCandle.LowPrice;
}
else
{
ShiftHistory(candle);
return;
}
var direction = 0;
// Long signal when the latest candle closes above the doji range.
if (candle.ClosePrice > dojiHigh)
{
direction = 1;
}
// Short signal when the latest candle closes below the doji range.
else if (candle.ClosePrice < dojiLow)
{
direction = -1;
}
if (direction != 0 && Volume > 0m)
{
if (direction > 0)
{
// Buy enough volume to cover a short position and establish the target long size.
var volume = Volume + Math.Max(0m, -Position);
if (volume > 0m)
BuyMarket(volume);
}
else
{
// Sell enough volume to cover a long position and establish the target short size.
var volume = Volume + Math.Max(0m, Position);
if (volume > 0m)
SellMarket(volume);
}
}
ShiftHistory(candle);
}
private decimal CalculatePipSize()
{
var step = Security?.PriceStep ?? 1m;
var digits = 0;
var value = step;
while (value < 1m && digits < 10)
{
value *= 10m;
digits++;
}
var multiplier = (digits == 3 || digits == 5) ? 10m : 1m;
return step * multiplier;
}
private static bool IsDoji(ICandleMessage candle, decimal threshold)
{
var body = Math.Abs(candle.OpenPrice - candle.ClosePrice);
return body <= threshold;
}
private void ShiftHistory(ICandleMessage candle)
{
// Maintain the three most recent completed candles for doji detection.
_threeAgoCandle = _twoAgoCandle;
_twoAgoCandle = _previousCandle;
_previousCandle = candle;
}
}
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, Unit, UnitTypes
from StockSharp.Algo.Strategies import Strategy
class doji_trader_strategy(Strategy):
def __init__(self):
super(doji_trader_strategy, self).__init__()
self._stop_loss_pips = self.Param("StopLossPips", 50.0)
self._take_profit_pips = self.Param("TakeProfitPips", 50.0)
self._start_hour = self.Param("StartHour", 8)
self._end_hour = self.Param("EndHour", 17)
self._maximum_doji_height = self.Param("MaximumDojiHeight", 1.0)
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(1)))
self._previous_candle = None
self._two_ago_candle = None
self._three_ago_candle = None
self._pip_size = 0.0
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
@property
def StopLossPips(self):
return self._stop_loss_pips.Value
@property
def TakeProfitPips(self):
return self._take_profit_pips.Value
@property
def StartHour(self):
return self._start_hour.Value
@property
def EndHour(self):
return self._end_hour.Value
@property
def MaximumDojiHeight(self):
return self._maximum_doji_height.Value
def OnStarted2(self, time):
super(doji_trader_strategy, self).OnStarted2(time)
self._pip_size = self._calculate_pip_size()
tp_unit = Unit(self.TakeProfitPips * self._pip_size, UnitTypes.Absolute) if self.TakeProfitPips > 0 and self._pip_size > 0 else Unit()
sl_unit = Unit(self.StopLossPips * self._pip_size, UnitTypes.Absolute) if self.StopLossPips > 0 and self._pip_size > 0 else Unit()
self.StartProtection(tp_unit, sl_unit)
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self._process_candle).Start()
def _process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
next_hour = candle.CloseTime.Hour
if next_hour < self.StartHour or next_hour >= self.EndHour:
self._shift_history(candle)
return
if self._two_ago_candle is None:
self._shift_history(candle)
return
pip_size = self._pip_size if self._pip_size > 0 else self._calculate_pip_size()
if pip_size <= 0:
pip_size = 0.0001
self._pip_size = pip_size
doji_height = self.MaximumDojiHeight * pip_size
doji_high = 0.0
doji_low = 0.0
if self._is_doji(self._two_ago_candle, doji_height):
doji_high = float(self._two_ago_candle.HighPrice)
doji_low = float(self._two_ago_candle.LowPrice)
elif self._three_ago_candle is not None and self._is_doji(self._three_ago_candle, doji_height):
doji_high = float(self._three_ago_candle.HighPrice)
doji_low = float(self._three_ago_candle.LowPrice)
else:
self._shift_history(candle)
return
direction = 0
if float(candle.ClosePrice) > doji_high:
direction = 1
elif float(candle.ClosePrice) < doji_low:
direction = -1
if direction != 0 and self.Volume > 0:
if direction > 0:
self.BuyMarket()
else:
self.SellMarket()
self._shift_history(candle)
def _calculate_pip_size(self):
sec = self.Security
step = float(sec.PriceStep) if sec is not None and sec.PriceStep is not None else 1.0
digits = 0
value = step
while value < 1.0 and digits < 10:
value *= 10.0
digits += 1
multiplier = 10.0 if (digits == 3 or digits == 5) else 1.0
return step * multiplier
def _is_doji(self, candle, threshold):
body = abs(float(candle.OpenPrice) - float(candle.ClosePrice))
return body <= threshold
def _shift_history(self, candle):
self._three_ago_candle = self._two_ago_candle
self._two_ago_candle = self._previous_candle
self._previous_candle = candle
def OnReseted(self):
super(doji_trader_strategy, self).OnReseted()
self._previous_candle = None
self._two_ago_candle = None
self._three_ago_candle = None
self._pip_size = 0.0
def CreateClone(self):
return doji_trader_strategy()