在 GitHub 上查看

Semilong WWW Forex Instruments Info 策略

概述

该策略复刻了 MetaTrader 专家顾问 “Semilong” 的交易逻辑。它同时观测当前买价与两个历史收盘价之间的距离,这两个收盘价之间相隔可配置的 K 线数量。当当前价格足够大幅度地低于(或高于)较新的参考收盘价,并且该收盘价又明显脱离更久远的收盘价时,策略会开仓做多或做空。头寸管理完全参照原始脚本,支持自定义止盈、止损、可选的移动止损,以及在连续亏损后自动降低仓位的自动手数模块。

信号生成

  • 历史偏移ShiftOne 决定第一个比较收盘价向前追溯多少根已完成的 K 线,ShiftTwo 在此基础上再增加额外的偏移量以获得第二个收盘价。
  • 偏离阈值MoveOnePoints 规定当前买价需要相对第一个收盘价偏离多少点,MoveTwoPoints 衡量两个历史收盘价之间的距离。
  • 多头条件:当当前买价至少比第一个收盘价低 MoveOnePoints 点,且第一个收盘价又至少比第二个收盘价高 MoveTwoPoints 点时触发。
  • 空头条件:当当前买价至少比第一个收盘价高 MoveOnePoints 点,且第一个收盘价至少比第二个收盘价低 MoveTwoPoints 点时触发。
  • 策略仅在 K 线收盘后评估信号;如果存在未完成的订单或可用保证金不足,则忽略信号。

头寸管理

  • 初始保护:策略不直接挂出止盈止损单,而是追踪入场价格,当价格朝有利方向运行 ProfitPoints(额外加上实时点差)或不利方向运行 LossPoints 时立即平仓。
  • 移动止损TrailingPoints 大于零时,策略会记录入场后的最佳价位,一旦价格回撤超过该距离便退出头寸。
  • 单一持仓:任意时刻只允许持有一个方向的头寸;在平仓订单成交前不会响应新的开仓信号。

仓位管理

  • 固定手数:当 UseAutoLot 关闭时,每笔交易使用 FixedVolume 手,并根据交易品种的最小/步长/最大手数自动调整。
  • 自动手数:开启自动手数后,会将自由保证金除以 AutoMarginDivider * 1000 并四舍五入到最近的整数手。如果出现至少两次连续亏损,则按照 lossStreak / DecreaseFactor 的比例减少下单手数,从而模拟原始 EA 的递减逻辑。
  • 计算出的手数会被限制在 FixedVolume 与 99 手之间,并对齐到品种支持的手数步长范围。

其他说明

  • 策略实时读取最佳买卖价计算点差,并在多空止盈目标中加上该点差,保持与原脚本一致的收益判定。
  • 自由保证金依据账户的 CurrentValue - BlockedValue 估算,当缺少保证金信息时退回到当前权益。
  • 未额外添加日志或图表,策略可直接在 StockSharp 设计器中优化或在 API 项目里运行。
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>
/// Reimplementation of the Semilong strategy that compares the current price with two historical closes.
/// Opens a position when the price sharply deviates from older levels and manages the trade with configurable stops,
/// take profit, trailing logic, and loss streak based position sizing.
/// </summary>
public class SemilongWwwForexInstrumentsInfoStrategy : Strategy
{
	private readonly StrategyParam<int> _profitPoints;
	private readonly StrategyParam<int> _lossPoints;
	private readonly StrategyParam<int> _shiftOne;
	private readonly StrategyParam<int> _moveOnePoints;
	private readonly StrategyParam<int> _shiftTwo;
	private readonly StrategyParam<int> _moveTwoPoints;
	private readonly StrategyParam<int> _decreaseFactor;
	private readonly StrategyParam<decimal> _fixedVolume;
	private readonly StrategyParam<int> _trailingPoints;
	private readonly StrategyParam<bool> _useAutoLot;
	private readonly StrategyParam<int> _autoMarginDivider;
	private readonly StrategyParam<DataType> _candleType;

	private readonly List<decimal> _closes = new();
	private decimal _pipSize;

	/// <summary>
	/// Initializes a new instance of the <see cref="SemilongWwwForexInstrumentsInfoStrategy"/> class.
	/// </summary>
	public SemilongWwwForexInstrumentsInfoStrategy()
	{
		_profitPoints = Param(nameof(ProfitPoints), 120)
		.SetDisplay("Take Profit (points)", "Distance in points for the take profit target", "Risk");

		_lossPoints = Param(nameof(LossPoints), 60)
		.SetDisplay("Stop Loss (points)", "Distance in points for the protective stop", "Risk");

		_shiftOne = Param(nameof(ShiftOne), 5)
		.SetNotNegative()
		.SetDisplay("Primary Shift", "Number of bars between the current close and the comparison close", "Signals");

		_moveOnePoints = Param(nameof(MoveOnePoints), 0)
		.SetNotNegative()
		.SetDisplay("Primary Move (points)", "Minimum deviation in points from the primary shifted close", "Signals");

		_shiftTwo = Param(nameof(ShiftTwo), 10)
		.SetNotNegative()
		.SetDisplay("Secondary Shift", "Additional bars added on top of the primary shift", "Signals");

		_moveTwoPoints = Param(nameof(MoveTwoPoints), 0)
		.SetNotNegative()
		.SetDisplay("Secondary Move (points)", "Minimum distance between the two shifted closes", "Signals");

		_decreaseFactor = Param(nameof(DecreaseFactor), 14)
		.SetNotNegative()
		.SetDisplay("Decrease Factor", "Divisor applied when shrinking the auto lot after losses", "Money Management");

		_fixedVolume = Param(nameof(FixedVolume), 1m)
		.SetDisplay("Fixed Volume", "Base volume used when auto lot is disabled", "Money Management");

		_trailingPoints = Param(nameof(TrailingPoints), 0)
		.SetNotNegative()
		.SetDisplay("Trailing Stop (points)", "Trailing stop distance in points", "Risk");

		_useAutoLot = Param(nameof(UseAutoLot), false)
		.SetDisplay("Use Auto Lot", "Enable dynamic position sizing based on free margin", "Money Management");

		_autoMarginDivider = Param(nameof(AutoMarginDivider), 7)
		.SetRange(1, int.MaxValue)
		.SetDisplay("Auto Margin Divider", "Divisor used to convert free margin into the lot size", "Money Management");

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
		.SetDisplay("Candle Type", "Time frame used for signal calculations", "General");
	}

	/// <summary>
	/// Take profit distance expressed in points.
	/// </summary>
	public int ProfitPoints
	{
		get => _profitPoints.Value;
		set => _profitPoints.Value = value;
	}

	/// <summary>
	/// Stop loss distance expressed in points.
	/// </summary>
	public int LossPoints
	{
		get => _lossPoints.Value;
		set => _lossPoints.Value = value;
	}

	/// <summary>
	/// Number of bars between the current candle and the primary comparison candle.
	/// </summary>
	public int ShiftOne
	{
		get => _shiftOne.Value;
		set => _shiftOne.Value = value;
	}

	/// <summary>
	/// Minimum deviation from the primary shifted close required before a trade is allowed.
	/// </summary>
	public int MoveOnePoints
	{
		get => _moveOnePoints.Value;
		set => _moveOnePoints.Value = value;
	}

	/// <summary>
	/// Additional bars added on top of the the primary shift for the secondary comparison.
	/// </summary>
	public int ShiftTwo
	{
		get => _shiftTwo.Value;
		set => _shiftTwo.Value = value;
	}

	/// <summary>
	/// Minimum distance in points between the two shifted closes.
	/// </summary>
	public int MoveTwoPoints
	{
		get => _moveTwoPoints.Value;
		set => _moveTwoPoints.Value = value;
	}

	/// <summary>
	/// Divisor used to reduce the calculated auto lot size after consecutive losses.
	/// </summary>
	public int DecreaseFactor
	{
		get => _decreaseFactor.Value;
		set => _decreaseFactor.Value = value;
	}

	/// <summary>
	/// Fixed trade volume used whenever auto lot sizing is disabled.
	/// </summary>
	public decimal FixedVolume
	{
		get => _fixedVolume.Value;
		set => _fixedVolume.Value = value;
	}

	/// <summary>
	/// Trailing stop distance expressed in points.
	/// </summary>
	public int TrailingPoints
	{
		get => _trailingPoints.Value;
		set => _trailingPoints.Value = value;
	}

	/// <summary>
	/// Gets or sets a value indicating whether the strategy calculates the lot size from free margin.
	/// </summary>
	public bool UseAutoLot
	{
		get => _useAutoLot.Value;
		set => _useAutoLot.Value = value;
	}

	/// <summary>
	/// Divider applied to free margin when auto lot sizing is enabled.
	/// </summary>
	public int AutoMarginDivider
	{
		get => _autoMarginDivider.Value;
		set => _autoMarginDivider.Value = value;
	}

	/// <summary>
	/// Candle type processed by the strategy.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

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

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();

		_closes.Clear();
		_pipSize = 0m;
	}

	/// <inheritdoc />
	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		_pipSize = Security?.PriceStep ?? 0m;
		if (_pipSize <= 0m)
			_pipSize = 1m;

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

		StartProtection(
			takeProfit: new Unit(2, UnitTypes.Percent),
			stopLoss: new Unit(1, UnitTypes.Percent));
	}

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

		_closes.Add(candle.ClosePrice);

		var totalShift = ShiftOne + ShiftTwo;
		if (_closes.Count > totalShift + 2)
			_closes.RemoveAt(0);

		if (_closes.Count <= totalShift)
			return;

		if (Position != 0m)
			return;

		var price = candle.ClosePrice;
		var shiftedOneValue = _closes[_closes.Count - 1 - ShiftOne];
		var shiftedTwoValue = _closes[_closes.Count - 1 - totalShift];

		var moveOne = MoveOnePoints * _pipSize;
		var moveTwo = MoveTwoPoints * _pipSize;

		var priceDelta = price - shiftedOneValue;
		var closeDelta = shiftedOneValue - shiftedTwoValue;

		var buySignal = priceDelta < -moveOne && closeDelta > moveTwo;
		var sellSignal = priceDelta > moveOne && closeDelta < -moveTwo;

		if (buySignal)
		{
			BuyMarket();
		}
		else if (sellSignal)
		{
			SellMarket();
		}
	}
}