在 GitHub 上查看
Doji Arrows 策略
概述
Doji Arrows Strategy 是将 MetaTrader 指标 Doji_arrows_expert1.mq4 移植到 StockSharp 的实现。策略思想是在市场出现中性十字星后,跟随下一根 K 线的方向性突破。当价格先形成接近开收价相等的十字星,再由下一根 K 线收盘突破十字星的高点或低点时,系统认为行情出现动量突破并顺势入场。
交易逻辑
- 信号窗口:策略始终缓存最近两根已完成的 K 线。较早的一根必须满足十字星条件,后一根用于确认突破方向。
- 十字星判定:若
|Open - Close| ≤ DojiBodyThresholdSteps * PriceStep,则判定该 K 线为十字星。默认阈值为 1 个最小报价单位,容许一跳的误差。
- 突破确认:
- 多头信号:第二根 K 线的收盘价高于十字星最高价加上
BreakoutBufferSteps 设定的缓冲。
- 空头信号:第二根 K 线的收盘价低于十字星最低价减去相同的缓冲。
- 单次触发:策略会记录上一根 K 线是否已经触发信号,只有在出现新的突破时才会再次下单,这与原版指标只画一次箭头的方式一致。
- 下单方式:
- 若出现与当前仓位相反的信号,策略会先平掉旧仓位,再以
Volume + |Position| 的手数在新方向建立头寸,实现快速翻仓。
- 在空仓状态下直接按突破方向发出市价单。
风险管理
- 初始止损:开仓后立即在距离入场价
InitialStopSteps * PriceStep 的位置记录内部止损线。
- 固定止盈:当价格到达
TakeProfitSteps * PriceStep 的距离时平仓获利。
- 移动止损:收益超过
TrailingStopSteps * PriceStep 时,止损会随着每根 K 线移动,锁定部分利润同时保留继续盈利的空间。
- 以上所有风险控制均以最小报价单位为基础,适用于不同品种。
参数
| 名称 |
说明 |
默认值 |
CandleType |
用于分析的 K 线类型/周期。 |
5 分钟 K 线 |
DojiBodyThresholdSteps |
判定十字星的最大实体(单位:报价步长)。 |
1 |
BreakoutBufferSteps |
突破确认所需的额外缓冲距离。 |
0 |
InitialStopSteps |
初始止损距离(步长)。 |
20 |
TakeProfitSteps |
固定止盈距离(步长)。 |
25 |
TrailingStopSteps |
移动止损距离(步长)。 |
10 |
所有参数均通过 StrategyParam<T> 暴露,便于界面展示与批量优化。
实现细节
- 采用高层级的蜡烛线订阅接口
SubscribeCandles().Bind(...),确保与框架推荐做法一致。
- 通过
_previousCandle 与 _twoCandlesAgo 保存状态,只在收到完整 K 线时才进行决策。
- 多头与空头的止损/止盈分别管理,并在平仓或缺乏行情数据时及时重置。
- 日志输出涵盖信号触发、止损、止盈等事件,方便回测阶段分析与排错。
使用建议
- 针对不同品种校准十字星阈值:若价格跳动较大,可适当增加
DojiBodyThresholdSteps。
- 在点差或噪声明显的市场上,可调节
BreakoutBufferSteps 过滤虚假突破。
- 多品种部署时,可结合账户层面的风险控制(如组合止损、交易时段限制)。
- 由于信号基于收盘数据,建议选择与交易节奏匹配的
CandleType(例如 1 分钟用于短线、15 分钟用于波段)。
using System;
using System.Collections.Generic;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Doji Arrows breakout strategy.
/// Detects doji candles (small body) and trades breakout of the doji range on the next candle.
/// Uses ATR to define what constitutes a small body.
/// </summary>
public class DojiArrowsBreakoutStrategy : Strategy
{
private readonly StrategyParam<int> _atrPeriod;
private readonly StrategyParam<decimal> _dojiThreshold;
private readonly StrategyParam<DataType> _candleType;
private decimal _prevHigh;
private decimal _prevLow;
private bool _prevWasDoji;
private bool _hasPrev;
public int AtrPeriod { get => _atrPeriod.Value; set => _atrPeriod.Value = value; }
public decimal DojiThreshold { get => _dojiThreshold.Value; set => _dojiThreshold.Value = value; }
public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
public DojiArrowsBreakoutStrategy()
{
_atrPeriod = Param(nameof(AtrPeriod), 14)
.SetDisplay("ATR Period", "ATR period for doji detection", "Indicators");
_dojiThreshold = Param(nameof(DojiThreshold), 0.3m)
.SetDisplay("Doji Threshold", "Max body/ATR ratio for doji", "Indicators");
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
.SetDisplay("Candle Type", "Candle timeframe", "General");
}
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities() => [(Security, CandleType)];
protected override void OnReseted() { base.OnReseted(); _prevHigh = 0m; _prevLow = 0m; _prevWasDoji = false; _hasPrev = false; }
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_hasPrev = false;
_prevWasDoji = false;
var atr = new AverageTrueRange { Length = AtrPeriod };
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(atr, ProcessCandle)
.Start();
}
private void ProcessCandle(ICandleMessage candle, decimal atr)
{
if (candle.State != CandleStates.Finished)
return;
if (atr <= 0)
return;
var body = Math.Abs(candle.ClosePrice - candle.OpenPrice);
var isDoji = body / atr < DojiThreshold;
if (_hasPrev && _prevWasDoji)
{
// Breakout above doji high
if (candle.ClosePrice > _prevHigh && Position <= 0)
{
if (Position < 0)
BuyMarket();
BuyMarket();
}
// Breakout below doji low
else if (candle.ClosePrice < _prevLow && Position >= 0)
{
if (Position > 0)
SellMarket();
SellMarket();
}
}
_prevHigh = candle.HighPrice;
_prevLow = candle.LowPrice;
_prevWasDoji = isDoji;
_hasPrev = true;
}
}
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.Indicators import AverageTrueRange
from StockSharp.Algo.Strategies import Strategy
class doji_arrows_breakout_strategy(Strategy):
def __init__(self):
super(doji_arrows_breakout_strategy, self).__init__()
self._atr_period = self.Param("AtrPeriod", 14) \
.SetDisplay("ATR Period", "ATR period for doji detection", "Indicators")
self._doji_threshold = self.Param("DojiThreshold", 0.3) \
.SetDisplay("Doji Threshold", "Max body/ATR ratio for doji", "Indicators")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4))) \
.SetDisplay("Candle Type", "Candle timeframe", "General")
self._prev_high = 0.0
self._prev_low = 0.0
self._prev_was_doji = False
self._has_prev = False
@property
def atr_period(self):
return self._atr_period.Value
@property
def doji_threshold(self):
return self._doji_threshold.Value
@property
def candle_type(self):
return self._candle_type.Value
def OnReseted(self):
super(doji_arrows_breakout_strategy, self).OnReseted()
self._prev_high = 0.0
self._prev_low = 0.0
self._prev_was_doji = False
self._has_prev = False
def OnStarted2(self, time):
super(doji_arrows_breakout_strategy, self).OnStarted2(time)
self._has_prev = False
self._prev_was_doji = False
atr = AverageTrueRange()
atr.Length = self.atr_period
subscription = self.SubscribeCandles(self.candle_type)
subscription.Bind(atr, self.process_candle).Start()
def process_candle(self, candle, atr):
if candle.State != CandleStates.Finished:
return
atr_val = float(atr)
if atr_val <= 0:
return
body = abs(float(candle.ClosePrice) - float(candle.OpenPrice))
is_doji = body / atr_val < self.doji_threshold
if self._has_prev and self._prev_was_doji:
if float(candle.ClosePrice) > self._prev_high and self.Position <= 0:
if self.Position < 0:
self.BuyMarket()
self.BuyMarket()
elif float(candle.ClosePrice) < self._prev_low and self.Position >= 0:
if self.Position > 0:
self.SellMarket()
self.SellMarket()
self._prev_high = float(candle.HighPrice)
self._prev_low = float(candle.LowPrice)
self._prev_was_doji = is_doji
self._has_prev = True
def CreateClone(self):
return doji_arrows_breakout_strategy()