在 GitHub 上查看

LazyBot V1 策略

概述

LazyBot V1 是从原始 MetaTrader 5 专家顾问移植到 StockSharp 的日线突破策略。策略在每个交易日根据前一日的最高价与最低价布置买入和卖出止损单,并通过固定距离的移动止损保护已有持仓。

运行逻辑

  1. 订阅并等待所选周期(默认日线)的完整蜡烛收盘。
  2. 新交易日开始时跳过周末,并在启用时间过滤的情况下确认当前服务器时间位于允许的交易时段。
  3. 取消策略之前生成的所有突破型挂单。
  4. 在前一日最高价上方放置买入止损单,在前一日最低价下方放置卖出止损单;Breakout Offset (pips) 参数可以让挂单离极值更远。
  5. 当任意挂单被触发后,按照 Stop Loss (pips) 的距离设定初始止损,并在价格继续向有利方向移动时及时收紧。
  6. 下一次下单前重新计算交易量,可以选择固定手数或者按账户权益的风险百分比确定。

参数说明

参数 说明
Candle Type 用于生成参考蜡烛的周期(默认日线)。
Bot Name 写入订单备注的文本,方便识别。
Stop Loss (pips) 初始止损和移动止损的距离。
Breakout Offset (pips) 在前日高低点基础上额外增加的突破距离。
Max Spread (pips) 放弃新挂单前允许的最大点差,0 表示关闭此过滤。
Use Trading Hours 是否启用交易时间过滤。
Start Hour 允许开始布置挂单的小时。
End Hour 停止布置新挂单的小时;若与 Start Hour 相同,则仅限制下界。
Use Risk % 启用风险百分比仓位管理。
Risk % 在启用 Use Risk % 时,按账户权益百分比计算的风险额度。
Fixed Volume 固定下单量;当为 0 时回退到全局 Volume(默认 0.01)。

风险控制

  • 移动止损始终保持在最新买价或卖价之后 Stop Loss (pips) 的距离,只有当价格创新高/新低时才会向盈利方向收紧。
  • 点差过滤可避免在市场流动性不足时继续布置突破单。
  • 风险仓位管理会将可承受风险金额(equity * Risk %)除以止损价差,并保证结果不会低于固定手数。

其他信息

  • 订单备注格式为 BotName;SymbolId;YYYYMMDD,便于追踪不同日期创建的挂单。
  • 策略订阅 Level1 数据以实时监控点差并更新移动止损。
  • 在每根蜡烛结束以及成交后都会重新计算并更新保护性止损,以贴近原版 EA 的行为。
using System;
using System.Collections.Generic;

using Ecng.Common;

using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Simplified from "LazyBot V1" MetaTrader expert.
/// Daily breakout strategy using previous N-bar high/low range.
/// Buys when price breaks above previous high, sells when below previous low.
/// </summary>
public class LazyBotV1Strategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _lookback;

	private readonly Queue<decimal> _highs = new();
	private readonly Queue<decimal> _lows = new();

	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	public int Lookback
	{
		get => _lookback.Value;
		set => _lookback.Value = value;
	}

	public LazyBotV1Strategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(60).TimeFrame())
			.SetDisplay("Candle Type", "Timeframe for breakout detection", "General");

		_lookback = Param(nameof(Lookback), 30)
			.SetGreaterThanZero()
			.SetDisplay("Lookback", "Number of bars for high/low range", "Indicators");
	}

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

		_highs.Clear();
		_lows.Clear();

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

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

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

		// Build range from previous bars (not including current)
		if (_highs.Count >= Lookback)
		{
			var highs = _highs.ToArray();
			var lows = _lows.ToArray();
			decimal highest = decimal.MinValue;
			decimal lowest = decimal.MaxValue;
			foreach (var h in highs)
				if (h > highest) highest = h;
			foreach (var l in lows)
				if (l < lowest) lowest = l;

			var close = candle.ClosePrice;
			var volume = Volume;
			if (volume <= 0)
				volume = 1;
			var padding = (highest - lowest) * 0.05m;

			if (close > highest + padding)
			{
				if (Position <= 0)
					BuyMarket(Position < 0 ? Math.Abs(Position) + volume : volume);
			}
			else if (close < lowest - padding)
			{
				if (Position >= 0)
					SellMarket(Position > 0 ? Math.Abs(Position) + volume : volume);
			}
		}

		_highs.Enqueue(candle.HighPrice);
		_lows.Enqueue(candle.LowPrice);

		if (_highs.Count > Lookback)
		{
			_highs.Dequeue();
			_lows.Dequeue();
		}
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		_highs.Clear();
		_lows.Clear();

		base.OnReseted();
	}
}