在 GitHub 上查看

Coin Flipping 策略

概述

Coin Flipping 策略是对经典 MetaTrader 智能交易程序的直接移植,它通过“掷硬币”来决定买入还是卖出。每当上一根K线完成并且策略没有持仓时,就会触发一次新的决定,因此交易形成连续且相互独立的序列。StockSharp 版本刻意保持这种极简行为:同时只持有一笔仓位,并且为每笔交易设置对称的止盈和止损距离(以点数表示)。

尽管逻辑非常天真,这个示例展示了如何把体量很小的 EA 转换到 StockSharp 的高级 API 中。它适合作为示范,说明如何配置数据订阅、资金管理辅助函数以及保护性订单。

交易逻辑

  1. 策略启动时使用当前系统计时器为随机数生成器设定种子,对应原始 MQL 代码中的 MathSrand(GetTickCount())
  2. 对于每一根收盘完成的K线(默认周期为1分钟,可替换为任何蜡烛类型),策略会检查是否允许交易并确认当前没有持仓。
  3. 在空仓状态下,随机数生成器产生 0 或 1。结果为 0 时发送市价买单,结果为 1 时发送市价卖单。下单数量根据设定的风险百分比和止损距离动态计算。
  4. 通过 StartProtection 创建的保护性订单会为每笔仓位自动附加止损与止盈,从而无需手动管理退出。

没有其他过滤条件:一旦上一笔交易平仓,下一根K线立即重新“掷硬币”。

仓位规模

在 StockSharp 版本中,仓位规模公式被改写为适应组合资产。风险金额的计算方式为 Portfolio.CurrentValue * RiskPercent / 100,即按组合市值的一定比例承担风险。该金额除以换算成价格单位的止损距离(使用标的的最小价格跳动转换点数)即可得到下单数量。随后,帮助函数会按照 VolumeStep 的粒度对数量进行取整,并遵守标的的 MinVolumeMaxVolume 限制。

这样既保留了原代码“每笔交易冒固定比例资金”的思路,又保证委托数量符合 StockSharp 对证券的约束。

参数

参数 说明 默认值 备注
RiskPercent 每笔交易投入的组合资金百分比。 2 数值越大下单量越大,反之越小。
TakeProfitPips 入场价到止盈位的距离(点)。 20 通过价格步长转换为绝对价格并传递给 StartProtection
StopLossPips 入场价到止损位的距离(点)。 10 同样会转换为价格单位,并用于仓位规模计算。
CandleType 用于驱动决策循环的蜡烛订阅。 1 分钟周期 可替换为任意 StockSharp 蜡烛类型;周期越大交易频率越低。

风险管理

StartProtectionOnStarted 中被调用一次,使用计算好的止盈和止损距离。之后由 StockSharp 自动维护保护性订单,模拟了 MQL OrderSend 函数中止盈止损参数的效果。由于策略只在 Position == 0 时才开仓,无需手动撤单或重建保护性订单;仓位平仓后平台会自动取消它们。

实现细节

  • 使用高级 SubscribeCandles().Bind(...) 模式处理K线,使代码简洁且易读。
  • 日志记录会输出方向和下单数量,便于回测时观察伪随机生成器的表现。
  • 仓位规模调整会考虑 VolumeStepMinVolumeMaxVolume,确保所有订单满足交易所规则。
  • 按仓库要求,代码中的注释全部使用英文,并遵循既定结构。

使用建议

  • 由于方向完全随机,该策略并不追求长期盈利,更多用于演示或测试基础设施。
  • 请确保绑定的组合 CurrentValue 为正值,否则风险计算结果为零,将不会下单。
  • 如果想降低或提高“掷硬币”的频率,可调整蜡烛类型(例如改成小时级别或逐笔数据)。
  • 在优化时可以尝试不同的止盈止损距离,或降低风险百分比,以控制潜在回撤。
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>
/// Randomized coin flipping strategy that alternates between buying and selling based on a pseudo-random generator.
/// Mimics the original MetaTrader expert advisor by opening a single position at a time with symmetric risk controls.
/// </summary>
public class CoinFlippingStrategy : Strategy
{
	private readonly StrategyParam<decimal> _riskPercent;
	private readonly StrategyParam<int> _takeProfitPips;
	private readonly StrategyParam<int> _stopLossPips;
	private readonly StrategyParam<DataType> _candleType;

	private Random _random;
	private decimal _priceStep;
	private decimal _takeProfitDistance;
	private decimal _stopLossDistance;
	private decimal _entryPrice;
	private decimal? _stopPrice;
	private decimal? _takePrice;

	/// <summary>
	/// Portfolio share allocated to every trade in percent.
	/// </summary>
	public decimal RiskPercent
	{
		get => _riskPercent.Value;
		set => _riskPercent.Value = value;
	}

	/// <summary>
	/// Take profit distance measured in pips.
	/// </summary>
	public int TakeProfitPips
	{
		get => _takeProfitPips.Value;
		set => _takeProfitPips.Value = value;
	}

	/// <summary>
	/// Stop loss distance measured in pips.
	/// </summary>
	public int StopLossPips
	{
		get => _stopLossPips.Value;
		set => _stopLossPips.Value = value;
	}

	/// <summary>
	/// Candle type used for scheduling trade attempts.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Initialize strategy parameters.
	/// </summary>
	public CoinFlippingStrategy()
	{
		_riskPercent = Param(nameof(RiskPercent), 2m)
			.SetGreaterThanZero()
			.SetDisplay("Risk %", "Portfolio percentage allocated per trade", "Risk Management")
			
			.SetOptimize(1m, 10m, 1m);

		_takeProfitPips = Param(nameof(TakeProfitPips), 5000)
			.SetGreaterThanZero()
			.SetDisplay("Take Profit (pips)", "Target distance expressed in pips", "Risk Management")

			.SetOptimize(10, 50, 5);

		_stopLossPips = Param(nameof(StopLossPips), 3000)
			.SetGreaterThanZero()
			.SetDisplay("Stop Loss (pips)", "Protective stop distance expressed in pips", "Risk Management")

			.SetOptimize(5, 30, 5);

		_candleType = Param(nameof(CandleType), TimeSpan.FromDays(1).TimeFrame())
			.SetDisplay("Candle Type", "Candle type used for trade timing", "Data");
	}

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

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

		// Reset cached state when the strategy is reset.
		_random = null;
		_priceStep = 0m;
		_takeProfitDistance = 0m;
		_stopLossDistance = 0m;
		_entryPrice = 0m;
		_stopPrice = null;
		_takePrice = null;
	}

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

		// Seed the pseudo-random generator similarly to the MQL expert.
		_random = new Random(System.Environment.TickCount);

		// Determine price step information for translating pips into price units.
		_priceStep = Security?.PriceStep ?? 1m;
		if (_priceStep <= 0m)
			_priceStep = 1m;

		_takeProfitDistance = TakeProfitPips * _priceStep;
		_stopLossDistance = StopLossPips * _priceStep;

		// Subscribe to candle data to trigger decision making once per bar.
		var subscription = SubscribeCandles(CandleType);
		subscription.Bind(ProcessCandle).Start();

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

	private void ProcessCandle(ICandleMessage candle)
	{
		// Only use completed candles to avoid duplicate executions while a bar is forming.
		if (candle.State != CandleStates.Finished)
			return;

		// Check risk management first.
		if (Position > 0)
		{
			if (_stopPrice.HasValue && candle.LowPrice <= _stopPrice.Value)
			{
				SellMarket(Position);
				ResetTargets();
			}
			else if (_takePrice.HasValue && candle.HighPrice >= _takePrice.Value)
			{
				SellMarket(Position);
				ResetTargets();
			}
		}
		else if (Position < 0)
		{
			if (_stopPrice.HasValue && candle.HighPrice >= _stopPrice.Value)
			{
				BuyMarket(Math.Abs(Position));
				ResetTargets();
			}
			else if (_takePrice.HasValue && candle.LowPrice <= _takePrice.Value)
			{
				BuyMarket(Math.Abs(Position));
				ResetTargets();
			}
		}

		// The strategy maintains at most one position at a time.
		if (Position != 0)
			return;

		if (_random == null)
			return;

		var entryPrice = candle.ClosePrice;
		if (entryPrice <= 0m)
			return;

		var volume = CalculateOrderVolume(entryPrice);
		if (volume <= 0m)
			return;

		var isBuy = _random.Next(0, 2) == 0;
		if (isBuy)
		{
			BuyMarket(volume);
			_entryPrice = entryPrice;
			_stopPrice = _stopLossDistance > 0m ? entryPrice - _stopLossDistance : null;
			_takePrice = _takeProfitDistance > 0m ? entryPrice + _takeProfitDistance : null;
		}
		else
		{
			SellMarket(volume);
			_entryPrice = entryPrice;
			_stopPrice = _stopLossDistance > 0m ? entryPrice + _stopLossDistance : null;
			_takePrice = _takeProfitDistance > 0m ? entryPrice - _takeProfitDistance : null;
		}
	}

	private decimal CalculateOrderVolume(decimal entryPrice)
	{
		var balance = Portfolio?.CurrentValue ?? 0m;
		if (balance <= 0m)
			return 0m;

		var riskAmount = balance * RiskPercent / 100m;
		if (riskAmount <= 0m)
			return 0m;

		var stopDistance = _stopLossDistance;
		if (stopDistance <= 0m)
		{
			stopDistance = StopLossPips * _priceStep;
		}

		if (stopDistance <= 0m)
			return 0m;

		// Risk per unit equals the stop distance; divide to get the number of contracts.
		var rawVolume = riskAmount / stopDistance;
		var volume = NormalizeVolume(rawVolume);

		if (volume <= 0m)
		{
			volume = Volume > 0m ? Volume : 1m;
			volume = NormalizeVolume(volume);
		}

		return volume;
	}

	private decimal NormalizeVolume(decimal volume)
	{
		if (volume <= 0m)
			return 0m;

		var step = Security?.VolumeStep;
		if (step.HasValue && step.Value > 0m)
		{
			volume = Math.Floor(volume / step.Value) * step.Value;
		}

		return volume > 0m ? volume : 1m;
	}

	private void ResetTargets()
	{
		_entryPrice = 0m;
		_stopPrice = null;
		_takePrice = null;
	}
}