在 GitHub 上查看

WOC 0.1.2 动能策略

概述

本策略是 MetaTrader 专家顾问“WOC.0.1.2”的 StockSharp 高级 API 版本。策略订阅 Level 1 买一/卖一报价,监控卖一价格的快速连续跳动。当卖一价格在指定时间窗口内连续上涨或下跌达到设定次数时,系统会沿突破方向发送市价单。策略一次只会持有一个方向的仓位,与原始 MQL 代码保持一致。

数据与执行

  • 行情来源:Level 1 买一价和卖一价,不依赖 K 线或其他技术指标。
  • 订单执行:使用市价单。止损与移动止损在策略内部通过实时比较买一/卖一来模拟。

信号逻辑

  1. 保存最近的卖一价格,并统计连续刷新更高价(上涨序列)或更低价(下跌序列)的次数。
  2. 当任一序列的计数达到 SequenceLength 时,检查该序列持续的时间是否不超过 SequenceTimeoutSeconds 秒。
  3. 如果下跌序列的计数大于上涨序列,则开空单;否则开多单。该判断完全复刻了原始 MQL 中“比较 up 与 down 计数再决定方向”的做法。
  4. 每次尝试开仓后都会重置所有计数,以便下一次信号从零开始累计。

仓位管理

  • 初始止损:开仓后立即记录一个止损价,距离当前买一价(多头)或卖一价(空头)StopLossTicks 个价格步长。
  • 移动止损:当浮盈超过 TrailingStopTicks 个价格步长时,把止损价跟随到距离当前买一/卖一 TrailingStopTicks 的位置,并确保止损仍至少距离当前价两个移动步长。这一点与 MQL 策略中 “StopLoss < Bid - 2 * TrailingStop” 的条件完全一致。
  • 离场方式:当监控到买一/卖一穿越保存的止损价时,立即通过市价单平仓,同时重置内部状态。

仓位规模

策略提供两种下单量模式:

  • 固定手数:使用 LotSize 参数作为订单量。
  • 自动手数:开启 UseAutoLotSizing 后,根据账户资金在下表中的区间映射出对应的下单量。账户资金读取自 Portfolio.CurrentValue,若不可用则退回 Portfolio.BeginValue
账户资金(大于) 下单量
0(默认) LotSize
200 0.04
300 0.05
400 0.06
500 0.07
600 0.08
700 0.09
800 0.10
900 0.20
1 000 0.30
2 000 0.40
3 000 0.50
4 000 0.60
5 000 0.70
6 000 0.80
7 000 0.90
8 000 1.00
9 000 2.00
10 000 3.00
11 000 4.00
12 000 5.00
13 000 6.00
14 000 7.00
15 000 8.00
20 000 9.00
30 000 10.00
40 000 11.00
50 000 12.00
60 000 13.00
70 000 14.00
80 000 15.00
90 000 16.00
100 000 17.00
110 000 18.00
120 000 19.00
130 000 20.00

参数说明

  • StopLossTicks:以价格步长表示的初始止损距离。
  • TrailingStopTicks:以价格步长表示的移动止损距离,设为 0 可关闭移动止损。
  • SequenceLength:连续刷新卖一价格所需的次数,达到后触发交易。
  • SequenceTimeoutSeconds:允许序列持续的最长时间(秒)。
  • LotSize:未启用自动手数时使用的固定下单量。
  • UseAutoLotSizing:是否按上表的资金区间自动调整下单量。

使用建议

  • 适用于卖一价格变动频繁的高流动性品种,建议使用 tick 级别数据源进行回测。
  • 该策略只处理对冲账户模式,一次仅持有一个净头寸方向。
  • 请确保 Security.PriceStep 已正确设置,否则止损与移动止损会退化为以 1 货币单位为步长的计算。
  • 策略不使用蜡烛或指标,在延迟较低的环境中表现最佳。
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>
/// Momentum strategy based on WOC 0.1.2 concept.
/// Detects consecutive candle close runs in one direction and enters on breakout.
/// Uses ATR-based stop loss and trailing stop.
/// </summary>
public class Woc012Strategy : Strategy
{
	private readonly StrategyParam<int> _sequenceLength;
	private readonly StrategyParam<decimal> _stopLossAtrMult;
	private readonly StrategyParam<decimal> _trailingAtrMult;
	private readonly StrategyParam<int> _atrPeriod;
	private readonly StrategyParam<DataType> _candleType;

	private decimal _prevClose;
	private int _upCount;
	private int _downCount;
	private decimal _entryPrice;
	private decimal? _stopPrice;

	public int SequenceLength { get => _sequenceLength.Value; set => _sequenceLength.Value = value; }
	public decimal StopLossAtrMult { get => _stopLossAtrMult.Value; set => _stopLossAtrMult.Value = value; }
	public decimal TrailingAtrMult { get => _trailingAtrMult.Value; set => _trailingAtrMult.Value = value; }
	public int AtrPeriod { get => _atrPeriod.Value; set => _atrPeriod.Value = value; }
	public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }

	public Woc012Strategy()
	{
		_sequenceLength = Param(nameof(SequenceLength), 6)
			.SetGreaterThanZero()
			.SetDisplay("Sequence Length", "Consecutive bars in same direction to trigger entry", "Signals");

		_stopLossAtrMult = Param(nameof(StopLossAtrMult), 1.5m)
			.SetGreaterThanZero()
			.SetDisplay("SL ATR Mult", "Stop loss as ATR multiple", "Risk");

		_trailingAtrMult = Param(nameof(TrailingAtrMult), 1.0m)
			.SetGreaterThanZero()
			.SetDisplay("Trail ATR Mult", "Trailing stop as ATR multiple", "Risk");

		_atrPeriod = Param(nameof(AtrPeriod), 14)
			.SetGreaterThanZero()
			.SetDisplay("ATR Period", "ATR calculation length", "Risk");

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(30).TimeFrame())
			.SetDisplay("Candle Type", "Candle timeframe", "General");
	}

	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		return [(Security, CandleType)];
	}

	protected override void OnReseted()
	{
		base.OnReseted();
		_prevClose = 0;
		_upCount = 0;
		_downCount = 0;
		_entryPrice = 0;
		_stopPrice = null;
	}

	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		var atr = new AverageTrueRange { Length = AtrPeriod };

		var subscription = SubscribeCandles(CandleType);
		subscription
			.Bind(atr, ProcessCandle)
			.Start();

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawOwnTrades(area);
		}
	}

	private void ProcessCandle(ICandleMessage candle, decimal atr)
	{
		if (candle.State != CandleStates.Finished)
			return;

		if (!IsFormedAndOnlineAndAllowTrading())
			return;

		var close = candle.ClosePrice;

		// Track consecutive direction
		if (_prevClose > 0)
		{
			if (close > _prevClose)
			{
				_upCount++;
				_downCount = 0;
			}
			else if (close < _prevClose)
			{
				_downCount++;
				_upCount = 0;
			}
			else
			{
				_upCount = 0;
				_downCount = 0;
			}
		}

		_prevClose = close;

		// Manage existing position
		if (Position != 0)
		{
			if (Position > 0)
			{
				// Trail up
				var trail = close - TrailingAtrMult * atr;
				if (_stopPrice == null || trail > _stopPrice)
					_stopPrice = trail;

				if (close <= _stopPrice)
				{
					SellMarket(Math.Abs(Position));
					_stopPrice = null;
					_entryPrice = 0;
					return;
				}
			}
			else
			{
				// Trail down
				var trail = close + TrailingAtrMult * atr;
				if (_stopPrice == null || trail < _stopPrice)
					_stopPrice = trail;

				if (close >= _stopPrice)
				{
					BuyMarket(Math.Abs(Position));
					_stopPrice = null;
					_entryPrice = 0;
					return;
				}
			}
		}

		// Entry: consecutive sequence completed
		if (_upCount >= SequenceLength && Position <= 0)
		{
			var vol = Volume + Math.Abs(Position);
			BuyMarket(vol);
			_entryPrice = close;
			_stopPrice = close - StopLossAtrMult * atr;
			_upCount = 0;
		}
		else if (_downCount >= SequenceLength && Position >= 0)
		{
			var vol = Volume + Math.Abs(Position);
			SellMarket(vol);
			_entryPrice = close;
			_stopPrice = close + StopLossAtrMult * atr;
			_downCount = 0;
		}
	}
}