Doji Arrows 策略
策略概述
Doji Arrows 策略是将 MetaTrader 平台的同名 EA 转换到 StockSharp 高级 API 的实现。核心思想是在出现标准的十字星(Doji)后等待价格突破其高低点范围。十字星表示买卖力量暂时平衡,一旦下一根 K 线在十字星上方收盘说明多头取得主导,反之在下方收盘则说明空头占优。
- 策略只处理所选
CandleType订阅中的已完成 K 线。 - 通过比较上一根 K 线的开盘价与收盘价,判断其是否为十字星。当两者的绝对差值小于等于
DojiBodyPoints乘以品种的最小价格跳动时,视为十字星。若参数设为0,则使用一个价格跳动作为容差,复现 MQL5 版本中对开收盘相等的判定。 - 如果下一根 K 线在十字星最高价之上收盘,则提交买入市价单;若在十字星最低价之下收盘,则提交卖出市价单。若存在反向仓位,市价单的数量会自动平仓并在需要时反手。
整个流程与原始 EA 每个新柱开始时检查一次的行为保持一致。
风险控制
转换版本保留了原策略的保护机制:
- 止损:
StopLossPoints决定入场价格与初始止损之间的距离,单位为价格跳动。为0时不放置固定止损。 - 止盈:
TakeProfitPoints指定目标利润的距离(价格跳动)。为0时不设定止盈位。 - 追踪止损:
TrailingStopPoints与TrailingStepPoints组合还原了追踪逻辑。当浮动利润超过TrailingStopPoints + TrailingStepPoints时,止损会被移动到距离最新收盘价TrailingStopPoints的位置(多头使用最高收盘价,空头使用最低收盘价)。仅当TrailingStopPoints大于零时启用。
策略在每根已完成 K 线之后检查是否触及止损或止盈。当 K 线的最高价或最低价突破任一保护价格时,立即以市价单平仓,并重置保护数据。
参数说明
| 参数 | 默认值 | 说明 |
|---|---|---|
StopLossPoints |
30 |
初始止损距离,单位为价格跳动。 |
TakeProfitPoints |
90 |
止盈目标距离,单位为价格跳动。 |
TrailingStopPoints |
15 |
追踪止损距离,单位为价格跳动。 |
TrailingStepPoints |
5 |
在移动追踪止损前所需的额外利润,单位为价格跳动。 |
DojiBodyPoints |
1 |
判断上一根 K 线是否为十字星的最大实体大小(价格跳动)。0 表示使用一个价格跳动的容差。 |
CandleType |
1 小时 |
用于生成信号的 K 线类型。 |
实现细节
- 通过
SubscribeCandles(CandleType).Bind(ProcessCandle)订阅蜡烛数据,仅保存最近一根完成的 K 线。 - 价格跳动从
Security?.PriceStep读取;若数据源未提供,则回退到1,确保策略能在合成或历史数据上运行。 - 每次开仓都会重新计算保护价格,追踪止损即使在禁用固定止损时也能建立止损位,从而复现 MQL 版本“零起步”的追踪行为。
- 所有交易均使用市价单,保持与原 EA 追求即时成交的思路一致。
使用建议
- 在启动策略前配置
Security、Portfolio以及Volume属性。 - 根据交易品种的报价精度调整各个以点数表示的参数,尤其是存在小数点报价的外汇或差价合约。
- 若需要更复杂的仓位管理,可结合 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>
/// Doji breakout strategy with optional fixed and trailing protection.
/// </summary>
public class DojiArrowsStrategy : Strategy
{
private readonly StrategyParam<decimal> _stopLossPoints;
private readonly StrategyParam<decimal> _takeProfitPoints;
private readonly StrategyParam<decimal> _trailingStopPoints;
private readonly StrategyParam<decimal> _trailingStepPoints;
private readonly StrategyParam<decimal> _dojiBodyPoints;
private readonly StrategyParam<DataType> _candleType;
private bool _hasPreviousCandle;
private decimal _prevOpen;
private decimal _prevClose;
private decimal _prevHigh;
private decimal _prevLow;
private decimal? _entryPrice;
private decimal? _stopPrice;
private decimal? _takePrice;
public decimal StopLossPoints
{
get => _stopLossPoints.Value;
set => _stopLossPoints.Value = value;
}
public decimal TakeProfitPoints
{
get => _takeProfitPoints.Value;
set => _takeProfitPoints.Value = value;
}
public decimal TrailingStopPoints
{
get => _trailingStopPoints.Value;
set => _trailingStopPoints.Value = value;
}
public decimal TrailingStepPoints
{
get => _trailingStepPoints.Value;
set => _trailingStepPoints.Value = value;
}
public decimal DojiBodyPoints
{
get => _dojiBodyPoints.Value;
set => _dojiBodyPoints.Value = value;
}
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
public DojiArrowsStrategy()
{
_stopLossPoints = Param(nameof(StopLossPoints), 30m)
.SetNotNegative()
.SetDisplay("Stop Loss Points", "Stop loss distance in price steps.", "Risk")
;
_takeProfitPoints = Param(nameof(TakeProfitPoints), 90m)
.SetNotNegative()
.SetDisplay("Take Profit Points", "Take profit distance in price steps.", "Risk")
;
_trailingStopPoints = Param(nameof(TrailingStopPoints), 15m)
.SetNotNegative()
.SetDisplay("Trailing Stop Points", "Trailing distance in price steps.", "Risk")
;
_trailingStepPoints = Param(nameof(TrailingStepPoints), 5m)
.SetNotNegative()
.SetDisplay("Trailing Step Points", "Minimum profit before the trailing stop moves.", "Risk")
;
_dojiBodyPoints = Param(nameof(DojiBodyPoints), 1m)
.SetNotNegative()
.SetDisplay("Doji Body Points", "Maximum difference between open and close to treat the candle as a doji.", "Pattern")
;
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Time frame used for signal generation.", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_hasPreviousCandle = false;
_prevOpen = 0m;
_prevClose = 0m;
_prevHigh = 0m;
_prevLow = 0m;
ResetProtection();
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
SubscribeCandles(CandleType)
.Bind(ProcessCandle)
.Start();
StartProtection(
takeProfit: new Unit(2, UnitTypes.Percent),
stopLoss: new Unit(1, UnitTypes.Percent)
);
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
ManageActivePosition(candle);
if (!_hasPreviousCandle)
{
CachePreviousCandle(candle);
return;
}
var step = Security?.PriceStep ?? 1m;
var tolerance = DojiBodyPoints <= 0m ? step : DojiBodyPoints * step;
var bodySize = Math.Abs(_prevOpen - _prevClose);
var isDoji = bodySize <= tolerance;
var breakoutUp = isDoji && candle.ClosePrice > _prevHigh;
var breakoutDown = isDoji && candle.ClosePrice < _prevLow;
if (breakoutUp && Position == 0)
{
BuyMarket();
}
else if (breakoutDown && Position == 0)
{
SellMarket();
}
CachePreviousCandle(candle);
}
private void ManageActivePosition(ICandleMessage candle)
{
if (Position == 0)
return;
var step = Security?.PriceStep ?? 1m;
var trailingDistance = TrailingStopPoints > 0m ? TrailingStopPoints * step : 0m;
var trailingStep = TrailingStepPoints > 0m ? TrailingStepPoints * step : 0m;
if (Position > 0)
{
if (trailingDistance > 0m && _entryPrice.HasValue)
{
var gain = candle.ClosePrice - _entryPrice.Value;
if (gain > trailingDistance + trailingStep)
{
var newStop = candle.ClosePrice - trailingDistance;
if (!_stopPrice.HasValue || newStop > _stopPrice.Value)
_stopPrice = newStop;
}
}
if (_stopPrice.HasValue && candle.LowPrice <= _stopPrice.Value)
{
SellMarket(Math.Abs(Position));
ResetProtection();
return;
}
if (_takePrice.HasValue && candle.HighPrice >= _takePrice.Value)
{
SellMarket(Math.Abs(Position));
ResetProtection();
return;
}
}
else if (Position < 0)
{
if (trailingDistance > 0m && _entryPrice.HasValue)
{
var gain = _entryPrice.Value - candle.ClosePrice;
if (gain > trailingDistance + trailingStep)
{
var newStop = candle.ClosePrice + trailingDistance;
if (!_stopPrice.HasValue || newStop < _stopPrice.Value)
_stopPrice = newStop;
}
}
if (_stopPrice.HasValue && candle.HighPrice >= _stopPrice.Value)
{
BuyMarket(Math.Abs(Position));
ResetProtection();
return;
}
if (_takePrice.HasValue && candle.LowPrice <= _takePrice.Value)
{
BuyMarket(Math.Abs(Position));
ResetProtection();
return;
}
}
}
private void InitializeProtection(decimal price, bool isLong, decimal step)
{
_entryPrice = price;
if (StopLossPoints > 0m)
{
var offset = StopLossPoints * step;
_stopPrice = isLong ? price - offset : price + offset;
}
else
{
_stopPrice = null;
}
if (TakeProfitPoints > 0m)
{
var offset = TakeProfitPoints * step;
_takePrice = isLong ? price + offset : price - offset;
}
else
{
_takePrice = null;
}
}
private void ResetProtection()
{
_entryPrice = null;
_stopPrice = null;
_takePrice = null;
}
private void CachePreviousCandle(ICandleMessage candle)
{
_prevOpen = candle.OpenPrice;
_prevClose = candle.ClosePrice;
_prevHigh = candle.HighPrice;
_prevLow = candle.LowPrice;
_hasPreviousCandle = true;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from StockSharp.Algo.Strategies import Strategy
from StockSharp.Messages import DataType, CandleStates
from System import TimeSpan, Math
class doji_arrows_strategy(Strategy):
def __init__(self):
super(doji_arrows_strategy, self).__init__()
self._stop_loss_points = self.Param("StopLossPoints", 30.0)
self._take_profit_points = self.Param("TakeProfitPoints", 90.0)
self._trailing_stop_points = self.Param("TrailingStopPoints", 15.0)
self._trailing_step_points = self.Param("TrailingStepPoints", 5.0)
self._doji_body_points = self.Param("DojiBodyPoints", 1.0)
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5)))
self._has_previous_candle = False
self._prev_open = 0.0
self._prev_close = 0.0
self._prev_high = 0.0
self._prev_low = 0.0
self._entry_price = None
self._stop_price = None
self._take_price = None
@property
def CandleType(self):
return self._candle_type.Value
def OnStarted2(self, time):
super(doji_arrows_strategy, self).OnStarted2(time)
sub = self.SubscribeCandles(self.CandleType)
sub.Bind(self._process_candle).Start()
def _process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
self._manage_active_position(candle)
if not self._has_previous_candle:
self._cache_previous_candle(candle)
return
step = self._get_price_step()
tolerance = step if self._doji_body_points.Value <= 0 else self._doji_body_points.Value * step
body_size = abs(self._prev_open - self._prev_close)
is_doji = body_size <= tolerance
breakout_up = is_doji and float(candle.ClosePrice) > self._prev_high
breakout_down = is_doji and float(candle.ClosePrice) < self._prev_low
if breakout_up and self.Position == 0:
self.BuyMarket()
self._initialize_protection(float(candle.ClosePrice), True, step)
elif breakout_down and self.Position == 0:
self.SellMarket()
self._initialize_protection(float(candle.ClosePrice), False, step)
self._cache_previous_candle(candle)
def _manage_active_position(self, candle):
if self.Position == 0:
return
step = self._get_price_step()
trailing_distance = self._trailing_stop_points.Value * step if self._trailing_stop_points.Value > 0 else 0.0
trailing_step = self._trailing_step_points.Value * step if self._trailing_step_points.Value > 0 else 0.0
if self.Position > 0:
if trailing_distance > 0 and self._entry_price is not None:
gain = float(candle.ClosePrice) - self._entry_price
if gain > trailing_distance + trailing_step:
new_stop = float(candle.ClosePrice) - trailing_distance
if self._stop_price is None or new_stop > self._stop_price:
self._stop_price = new_stop
if self._stop_price is not None and float(candle.LowPrice) <= self._stop_price:
self.SellMarket(abs(self.Position))
self._reset_protection()
return
if self._take_price is not None and float(candle.HighPrice) >= self._take_price:
self.SellMarket(abs(self.Position))
self._reset_protection()
return
elif self.Position < 0:
if trailing_distance > 0 and self._entry_price is not None:
gain = self._entry_price - float(candle.ClosePrice)
if gain > trailing_distance + trailing_step:
new_stop = float(candle.ClosePrice) + trailing_distance
if self._stop_price is None or new_stop < self._stop_price:
self._stop_price = new_stop
if self._stop_price is not None and float(candle.HighPrice) >= self._stop_price:
self.BuyMarket(abs(self.Position))
self._reset_protection()
return
if self._take_price is not None and float(candle.LowPrice) <= self._take_price:
self.BuyMarket(abs(self.Position))
self._reset_protection()
return
def _initialize_protection(self, price, is_long, step):
self._entry_price = price
if self._stop_loss_points.Value > 0:
offset = self._stop_loss_points.Value * step
self._stop_price = price - offset if is_long else price + offset
else:
self._stop_price = None
if self._take_profit_points.Value > 0:
offset = self._take_profit_points.Value * step
self._take_price = price + offset if is_long else price - offset
else:
self._take_price = None
def _reset_protection(self):
self._entry_price = None
self._stop_price = None
self._take_price = None
def _cache_previous_candle(self, candle):
self._prev_open = float(candle.OpenPrice)
self._prev_close = float(candle.ClosePrice)
self._prev_high = float(candle.HighPrice)
self._prev_low = float(candle.LowPrice)
self._has_previous_candle = True
def _get_price_step(self):
step = float(self.Security.PriceStep) if self.Security is not None and self.Security.PriceStep is not None else 1.0
return step if step > 0 else 1.0
def OnReseted(self):
super(doji_arrows_strategy, self).OnReseted()
self._has_previous_candle = False
self._prev_open = 0.0
self._prev_close = 0.0
self._prev_high = 0.0
self._prev_low = 0.0
self._reset_protection()
def CreateClone(self):
return doji_arrows_strategy()