在 GitHub 上查看

Exp Hans Indicator Cloud System Tm Plus 策略

概述

Exp Hans Indicator Cloud System Tm Plus 是一套基于交易时段的突破策略,用于复刻原始 MQL5 智能交易系统。算法在指定周期的 K 线上跟踪 Hans 指标的颜色状态:当出现多头颜色(0/1)后又恢复到通道内部时开多仓;当出现空头颜色(3/4)后又回到通道内部时开空仓。所有决策都基于已完成的 K 线,并保留了 MQL 版本中的点数止损/止盈和持仓时间限制。

策略只处理 GetWorkingSecurities() 返回的单一品种与蜡烛序列。订单数量由策略的 Volume 属性与资金管理系数共同决定。

指标逻辑

  1. 将 K 线时间从经纪商时区(LocalTimeZone)转换到目标时区(DestinationTimeZone)。默认设置为 GMT+4,与原指标保持一致。
  2. 每个交易日采集两个伦敦时段区间:
    • 区间 1:目的地时间 04:00–08:00。其高/低点构成初始突破通道。
    • 区间 2:目的地时间 08:00–12:00。完成后会替换区间 1,供当日余下时间使用。
  3. 两个区间都会向上/向下各扩展 PipsForEntry 个点。点值根据标的的 PriceStep 计算,若小数位为 3 或 5,则等同于 MetaTrader 的 10 倍 point。
  4. K 线颜色与原指标完全一致:
    • 收盘价高于上轨 → 颜色 0(阳线)或 1(阴线)。
    • 收盘价低于下轨 → 颜色 4(阴线)或 3(阳线)。
    • 收盘价位于通道内部 → 中性颜色 2

交易规则

  • 入场:上一根已完成的 K 线为多头颜色(0/1),而最新一根不再是多头颜色时(且允许做多),在下一根 K 线开盘执行市价买入。若上一根为空头颜色(3/4)并随后恢复,则在允许做空的情况下开空仓。
  • 离场
    • 当上一根颜色反向(做多遇到 3/4,做空遇到 0/1)时平仓。
    • 启用 UseTimeExit 时,持仓时间超过 HoldingMinutes 自动平仓。
    • StopLossPointsTakeProfitPoints 提供点数止损/止盈。若标的缺少有效的 PriceStep,对应功能会被跳过。
  • 平仓始终优先于开仓,确保翻仓时先清掉已有头寸。

参数说明

参数 说明 默认值
MoneyManagement 每次下单使用的 Volume 比例,≤ 0 时退回到完整 Volume 0.1
MoneyMode 与原脚本一致的资金管理模式占位符,目前仅实现 Lot Lot
StopLossPoints / TakeProfitPoints 以点数表示的止损/止盈,设置为 0 可关闭。 1000 / 2000
DeviationPoints 可接受的最大滑点(点)。仅保留配置项,StockSharp 的市价单无法强制该限制。 10
AllowBuyEntries / AllowSellEntries 是否允许做多/做空入场。 true
AllowBuyExits / AllowSellExits 是否允许对多/空头自动平仓。 true
UseTimeExit 启用时间止损。 true
HoldingMinutes 最大持仓时间(分钟)。 1500
PipsForEntry 区间上下扩展的点数。 100
SignalBar 信号所使用的已完成 K 线偏移。建议 ≥ 1 与 MT5 行为保持一致。 1
LocalTimeZone 经纪商服务器时区(小时)。 0
DestinationTimeZone 指标使用的目标时区。 4
CandleType 计算 Hans 指标的蜡烛类型。 30 分钟线

资金管理与执行

  • 订单数量 = Volume * MoneyManagement,并按 VolumeStep 四舍五入。若结果 ≤ 0,则使用一个 VolumeStep 的最小量。
  • 反向信号出现时,会一次性发送包含新仓位量和旧仓位反向量的市价单,等效于原 MQL 脚本中的 BuyPositionOpen/SellPositionOpen
  • 每次入场都会重新计算止损/止盈,并在平仓后清除。

使用建议

  1. 选择带有有效 PriceStepDecimalsVolumeStep 元数据的交易品种。
  2. 在启动前设置策略 Volume,资金管理系数会基于该值计算下单量。
  3. 选择与 MT5 一致的周期(默认 M30),所有判断均基于收盘 K 线。
  4. 如数据源时区与默认 GMT+4 不同,请调整 LocalTimeZone/DestinationTimeZone 以匹配原始指标。
  5. 关注日志信息:若缺少点值数据,风险控制会被禁用。

实现细节

  • 使用 SubscribeCandles 高级接口处理完成 K 线,无需手动维护指标缓冲区。
  • 每根 K 线结束时重新计算有效区间,仅保留当日最新结果,不创建额外历史集合。
  • DeviationPoints 仅作兼容保留,StockSharp 市价订单无法限制滑点。
  • OnReseted() 中重置内部状态,便于重复回测。

限制

  • 当前实现仅支持 SignalBar ≥ 1。若需要 0,需在高频 Tick 数据下实现额外逻辑。
  • MoneyMode 中除 Lot 以外的模式尚未实现,如需使用请自行扩展 GetOrderVolume()
  • 若标的缺少有效的 PriceStep,所有基于点的距离(止损、止盈、区间扩展)都会被跳过。
namespace StockSharp.Samples.Strategies;

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;

/// <summary>
/// Port of the Exp_Hans_Indicator_Cloud_System_Tm_Plus MQL5 expert advisor.
/// Replicates the Hans indicator breakout logic, including time-based exits and pip-based risk limits.
/// </summary>
public class ExpHansIndicatorCloudSystemTmPlusStrategy : Strategy
{
	private readonly StrategyParam<int> _maxHistory;

	private static readonly TimeSpan Session1Start = TimeSpan.FromHours(4);
	private static readonly TimeSpan Session1End = TimeSpan.FromHours(8);
	private static readonly TimeSpan Session2End = TimeSpan.FromHours(12);

	private readonly StrategyParam<decimal> _moneyManagement;
	private readonly StrategyParam<MoneyManagementModes> _moneyMode;
	private readonly StrategyParam<int> _stopLossPoints;
	private readonly StrategyParam<int> _takeProfitPoints;
	private readonly StrategyParam<int> _deviationPoints;
	private readonly StrategyParam<bool> _allowBuyEntries;
	private readonly StrategyParam<bool> _allowSellEntries;
	private readonly StrategyParam<bool> _allowBuyExits;
	private readonly StrategyParam<bool> _allowSellExits;
	private readonly StrategyParam<bool> _useTimeExit;
	private readonly StrategyParam<int> _holdingMinutes;
	private readonly StrategyParam<int> _pipsForEntry;
	private readonly StrategyParam<int> _signalBar;
	private readonly StrategyParam<int> _localTimeZone;
	private readonly StrategyParam<int> _destinationTimeZone;
	private readonly StrategyParam<int> _entryCooldownBars;
	private readonly StrategyParam<DataType> _candleType;

	private DailySessionState _dayState;
	private readonly List<int> _colorHistory = new();
	private decimal? _stopPrice;
	private decimal? _takePrice;
	private DateTimeOffset? _entryTime;
	private decimal _prevClosePrice;
	private int _cooldownRemaining;
	private bool _hasPrevClose;

	/// <summary>
	/// Enumeration matching the money management modes of the original expert.
	/// Currently only the Lot mode is applied; other options are reserved for future extensions.
	/// </summary>
	public enum MoneyManagementModes
	{
		FreeMargin,
		Balance,
		LossFreeMargin,
		LossBalance,
		Lot,
	}

	/// <summary>
	/// Portion of the base strategy volume used for each order.
	/// </summary>
	public decimal MoneyManagement
	{
		get => _moneyManagement.Value;
		set => _moneyManagement.Value = value;
	}

	/// <summary>
	/// Selected money management interpretation.
	/// </summary>
	public MoneyManagementModes MoneyMode
	{
		get => _moneyMode.Value;
		set => _moneyMode.Value = value;
	}

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

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

	/// <summary>
	/// Allowed execution deviation in points (kept for compatibility).
	/// </summary>
	public int DeviationPoints
	{
		get => _deviationPoints.Value;
		set => _deviationPoints.Value = value;
	}

	/// <summary>
	/// Enables long entries when the bullish breakout sequence completes.
	/// </summary>
	public bool AllowBuyEntries
	{
		get => _allowBuyEntries.Value;
		set => _allowBuyEntries.Value = value;
	}

	/// <summary>
	/// Enables short entries when the bearish breakout sequence completes.
	/// </summary>
	public bool AllowSellEntries
	{
		get => _allowSellEntries.Value;
		set => _allowSellEntries.Value = value;
	}

	/// <summary>
	/// Allows closing long positions on bearish Hans colors.
	/// </summary>
	public bool AllowBuyExits
	{
		get => _allowBuyExits.Value;
		set => _allowBuyExits.Value = value;
	}

	/// <summary>
	/// Allows closing short positions on bullish Hans colors.
	/// </summary>
	public bool AllowSellExits
	{
		get => _allowSellExits.Value;
		set => _allowSellExits.Value = value;
	}

	/// <summary>
	/// Enables the time-based exit filter.
	/// </summary>
	public bool UseTimeExit
	{
		get => _useTimeExit.Value;
		set => _useTimeExit.Value = value;
	}

	/// <summary>
	/// Maximum holding time in minutes before the position is liquidated.
	/// </summary>
	public int HoldingMinutes
	{
		get => _holdingMinutes.Value;
		set => _holdingMinutes.Value = value;
	}

	/// <summary>
	/// Number of pips added to the breakout range.
	/// </summary>
	public int PipsForEntry
	{
		get => _pipsForEntry.Value;
		set => _pipsForEntry.Value = value;
	}

	/// <summary>
	/// Number of closed candles used as signal offset.
	/// </summary>
	public int SignalBar
	{
		get => _signalBar.Value;
		set => _signalBar.Value = value;
	}

	/// <summary>
	/// Broker/server time zone in hours.
	/// </summary>
	public int LocalTimeZone
	{
		get => _localTimeZone.Value;
		set => _localTimeZone.Value = value;
	}

	/// <summary>
	/// Destination time zone defining the Hans breakout sessions.
	/// </summary>
	public int DestinationTimeZone
	{
		get => _destinationTimeZone.Value;
		set => _destinationTimeZone.Value = value;
	}

	/// <summary>
	/// Bars to wait after each entry before accepting a new one.
	/// </summary>
	public int EntryCooldownBars
	{
		get => _entryCooldownBars.Value;
		set => _entryCooldownBars.Value = value;
	}

	/// <summary>
	/// Candle series used for indicator calculations.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Maximum number of Hans color samples preserved for decision making.
	/// </summary>
	public int MaxHistory
	{
		get => _maxHistory.Value;
		set => _maxHistory.Value = value;
	}

	/// <summary>
	/// Initialize the strategy parameters with defaults matching the MQL5 inputs.
	/// </summary>
	public ExpHansIndicatorCloudSystemTmPlusStrategy()
	{
		_maxHistory = Param(nameof(MaxHistory), 1024)
			.SetGreaterThanZero()
			.SetDisplay("Max History", "Maximum number of Hans color entries stored", "Indicator");

		_moneyManagement = Param(nameof(MoneyManagement), 0.1m)
		.SetDisplay("Money Management", "Portion of the base volume traded per entry", "Risk");

		_moneyMode = Param(nameof(MoneyMode), MoneyManagementModes.Lot)
		.SetDisplay("Money Mode", "Interpretation of the money management value", "Risk");

		_stopLossPoints = Param(nameof(StopLossPoints), 1000)
		.SetDisplay("Stop Loss (points)", "Distance to the protective stop in points", "Risk")
		.SetNotNegative();

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 2000)
		.SetDisplay("Take Profit (points)", "Distance to the profit target in points", "Risk")
		.SetNotNegative();

		_deviationPoints = Param(nameof(DeviationPoints), 10)
		.SetDisplay("Execution Deviation", "Maximum acceptable slippage in points", "Orders")
		.SetNotNegative();

		_allowBuyEntries = Param(nameof(AllowBuyEntries), true)
		.SetDisplay("Enable Long Entries", "Allow opening long positions", "Signals");

		_allowSellEntries = Param(nameof(AllowSellEntries), true)
		.SetDisplay("Enable Short Entries", "Allow opening short positions", "Signals");

		_allowBuyExits = Param(nameof(AllowBuyExits), true)
		.SetDisplay("Enable Long Exits", "Allow automated long exits", "Signals");

		_allowSellExits = Param(nameof(AllowSellExits), true)
		.SetDisplay("Enable Short Exits", "Allow automated short exits", "Signals");

		_useTimeExit = Param(nameof(UseTimeExit), true)
		.SetDisplay("Use Time Exit", "Close positions after the holding period", "Risk");

		_holdingMinutes = Param(nameof(HoldingMinutes), 1500)
		.SetDisplay("Holding Minutes", "Maximum position lifetime in minutes", "Risk")
		.SetNotNegative();

		_pipsForEntry = Param(nameof(PipsForEntry), 5)
		.SetDisplay("Pips For Entry", "Offset added above/below the breakout range", "Indicator")
		.SetNotNegative();

		_signalBar = Param(nameof(SignalBar), 1)
		.SetDisplay("Signal Bar", "Closed candle offset used for signals", "Indicator")
		.SetNotNegative();

		_localTimeZone = Param(nameof(LocalTimeZone), 0)
		.SetDisplay("Local Time Zone", "Broker/server time zone", "Indicator");

		_destinationTimeZone = Param(nameof(DestinationTimeZone), 0)
		.SetDisplay("Destination Time Zone", "Target time zone for sessions", "Indicator");

		_entryCooldownBars = Param(nameof(EntryCooldownBars), 10)
			.SetDisplay("Entry Cooldown", "Bars to wait after an entry signal", "Risk")
			.SetGreaterThanZero();

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(30).TimeFrame())
		.SetDisplay("Candle Type", "Time frame used for Hans calculations", "Data");
	}

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

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

		_colorHistory.Clear();
		_dayState = null;
		_prevClosePrice = 0m;
		_cooldownRemaining = 0;
		_hasPrevClose = false;
		ResetPositionState();
	}

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

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

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

		if (Position == 0 && (_entryTime.HasValue || _stopPrice.HasValue || _takePrice.HasValue))
		ResetPositionState();

		if (_cooldownRemaining > 0)
			_cooldownRemaining--;

		UpdateDailyState(candle);

		var color = CalculateColor(candle);
		_colorHistory.Add(color);
		TrimHistory();

		var offset = Math.Max(1, SignalBar);
		if (_colorHistory.Count <= offset)
		return;

		var currentIndex = _colorHistory.Count - offset;
		if (currentIndex >= _colorHistory.Count)
		return;

		var currentColor = _colorHistory[currentIndex];
		var hasBands = TryGetActiveBands(out var upper, out var lower);
		var buyEntrySignal = false;
		var sellEntrySignal = false;

		if (hasBands && _hasPrevClose)
		{
			buyEntrySignal = AllowBuyEntries && _prevClosePrice <= upper && candle.ClosePrice > upper;
			sellEntrySignal = AllowSellEntries && _prevClosePrice >= lower && candle.ClosePrice < lower;
		}

		var buyExitSignal = AllowBuyExits && (IsLowerBreakout(currentColor) || (hasBands && candle.ClosePrice < lower));
		var sellExitSignal = AllowSellExits && (IsUpperBreakout(currentColor) || (hasBands && candle.ClosePrice > upper));

		if (Position > 0)
		{
			var exitByTime = UseTimeExit && HoldingMinutes > 0 && _entryTime.HasValue && candle.CloseTime - _entryTime.Value >= TimeSpan.FromMinutes(HoldingMinutes);
			var exitByStop = _stopPrice.HasValue && candle.LowPrice <= _stopPrice.Value;
			var exitByTarget = _takePrice.HasValue && candle.HighPrice >= _takePrice.Value;
			if (exitByTime || buyExitSignal || exitByStop || exitByTarget)
			{
				CloseLong();
			}
		}
		else if (Position < 0)
		{
			var exitByTime = UseTimeExit && HoldingMinutes > 0 && _entryTime.HasValue && candle.CloseTime - _entryTime.Value >= TimeSpan.FromMinutes(HoldingMinutes);
			var exitByStop = _stopPrice.HasValue && candle.HighPrice >= _stopPrice.Value;
			var exitByTarget = _takePrice.HasValue && candle.LowPrice <= _takePrice.Value;
			if (exitByTime || sellExitSignal || exitByStop || exitByTarget)
			{
				CloseShort();
			}
		}

		_prevClosePrice = candle.ClosePrice;
		_hasPrevClose = true;

		if (_cooldownRemaining > 0)
			return;

		if (_dayState != null && _dayState.EntryTaken)
			return;

		if (buyEntrySignal && Position <= 0)
		{
			EnterLong(candle);
		}
		else if (sellEntrySignal && Position >= 0)
		{
			EnterShort(candle);
		}
	}

	private void EnterLong(ICandleMessage candle)
	{
		var volume = GetOrderVolume();
		if (volume <= 0)
		return;

		var existingShort = Position < 0 ? Math.Abs(Position) : 0m;
		var totalVolume = volume + existingShort;
		if (totalVolume <= 0)
		return;

		BuyMarket(totalVolume);
		_cooldownRemaining = EntryCooldownBars;
		_dayState!.EntryTaken = true;

		_entryTime = candle.CloseTime != default ? candle.CloseTime : candle.OpenTime;

		var pipSize = GetPipSize();
		if (pipSize <= 0)
		{
			_stopPrice = null;
			_takePrice = null;
			return;
		}

		_stopPrice = StopLossPoints > 0 ? candle.ClosePrice - pipSize * StopLossPoints : null;
		_takePrice = TakeProfitPoints > 0 ? candle.ClosePrice + pipSize * TakeProfitPoints : null;
	}

	private void EnterShort(ICandleMessage candle)
	{
		var volume = GetOrderVolume();
		if (volume <= 0)
		return;

		var existingLong = Position > 0 ? Math.Abs(Position) : 0m;
		var totalVolume = volume + existingLong;
		if (totalVolume <= 0)
		return;

		SellMarket(totalVolume);
		_cooldownRemaining = EntryCooldownBars;
		_dayState!.EntryTaken = true;

		_entryTime = candle.CloseTime != default ? candle.CloseTime : candle.OpenTime;

		var pipSize = GetPipSize();
		if (pipSize <= 0)
		{
			_stopPrice = null;
			_takePrice = null;
			return;
		}

		_stopPrice = StopLossPoints > 0 ? candle.ClosePrice + pipSize * StopLossPoints : null;
		_takePrice = TakeProfitPoints > 0 ? candle.ClosePrice - pipSize * TakeProfitPoints : null;
	}

	private void CloseLong()
	{
		var volume = Math.Abs(Position);
		if (volume <= 0)
		return;

		SellMarket(volume);
		ResetPositionState();
	}

	private void CloseShort()
	{
		var volume = Math.Abs(Position);
		if (volume <= 0)
		return;

		BuyMarket(volume);
		ResetPositionState();
	}

	private void UpdateDailyState(ICandleMessage candle)
	{
		var destOpen = ToDestinationTime(candle.OpenTime);
		var date = destOpen.Date;

		if (_dayState == null || _dayState.Date != date)
		{
			_dayState = new DailySessionState { Date = date };
		}

		var state = _dayState;
		var timeOfDay = destOpen.TimeOfDay;

		if (timeOfDay >= Session1Start && timeOfDay < Session1End)
		{
			UpdateSessionRange(state, candle.HighPrice, candle.LowPrice, true);
			state.Session1Completed = false;
		}
		else if (timeOfDay >= Session1End && timeOfDay < Session2End)
		{
			if (!state.Session1Completed && state.Session1High.HasValue && state.Session1Low.HasValue)
			state.Session1Completed = true;

			UpdateSessionRange(state, candle.HighPrice, candle.LowPrice, false);
			state.Session2Completed = false;
		}
		else
		{
			if (!state.Session1Completed && state.Session1High.HasValue && state.Session1Low.HasValue)
			state.Session1Completed = true;

			if (!state.Session2Completed && state.Session2High.HasValue && state.Session2Low.HasValue)
			state.Session2Completed = true;
		}
	}

	private int CalculateColor(ICandleMessage candle)
	{
		if (!TryGetActiveBands(out var upper, out var lower))
		return 2;

		if (candle.ClosePrice > upper)
		return candle.ClosePrice >= candle.OpenPrice ? 0 : 1;

		if (candle.ClosePrice < lower)
		return candle.ClosePrice <= candle.OpenPrice ? 4 : 3;

		return 2;
	}

	private bool TryGetActiveBands(out decimal upper, out decimal lower)
	{
		upper = 0m;
		lower = 0m;

		var pipSize = GetPipSize();
		if (pipSize <= 0)
		return false;

		if (_dayState == null)
		return false;

		if (_dayState.Session2Completed && _dayState.Session2High.HasValue && _dayState.Session2Low.HasValue)
		{
			upper = _dayState.Session2High.Value + pipSize * PipsForEntry;
			lower = _dayState.Session2Low.Value - pipSize * PipsForEntry;
			return true;
		}

		if (_dayState.Session1Completed && _dayState.Session1High.HasValue && _dayState.Session1Low.HasValue)
		{
			upper = _dayState.Session1High.Value + pipSize * PipsForEntry;
			lower = _dayState.Session1Low.Value - pipSize * PipsForEntry;
			return true;
		}

		return false;
	}

	private void UpdateSessionRange(DailySessionState state, decimal high, decimal low, bool isFirstSession)
	{
		if (isFirstSession)
		{
			state.Session1High = state.Session1High.HasValue ? Math.Max(state.Session1High.Value, high) : high;
			state.Session1Low = state.Session1Low.HasValue ? Math.Min(state.Session1Low.Value, low) : low;
		}
		else
		{
			state.Session2High = state.Session2High.HasValue ? Math.Max(state.Session2High.Value, high) : high;
			state.Session2Low = state.Session2Low.HasValue ? Math.Min(state.Session2Low.Value, low) : low;
		}
	}

	private decimal GetOrderVolume()
	{
		var step = Security?.VolumeStep ?? 1m;
		if (step <= 0)
		step = 1m;

		var baseVolume = Volume * MoneyManagement;
		if (baseVolume <= 0)
		baseVolume = Volume;

		var normalized = Math.Round(baseVolume / step) * step;
		if (normalized <= 0)
		normalized = step;

		return normalized;
	}

	private decimal GetPipSize()
	{
		if (Security?.PriceStep is decimal step && step > 0m)
		{
			var decimals = Security.Decimals;
			if (decimals == 3 || decimals == 5)
			return step * 10m;

			return step;
		}

		return 0.01m;
	}

	private void ResetPositionState()
	{
		_entryTime = null;
		_stopPrice = null;
		_takePrice = null;
	}

	private void TrimHistory()
	{
		if (_colorHistory.Count <= MaxHistory)
		return;

		var excess = _colorHistory.Count - MaxHistory;
		_colorHistory.RemoveRange(0, excess);
	}

	private DateTimeOffset ToDestinationTime(DateTimeOffset time)
	{
		var shift = TimeSpan.FromHours(LocalTimeZone - DestinationTimeZone);
		return time - shift;
	}

	private static bool IsUpperBreakout(int? color) => color is 0 or 1;

	private static bool IsLowerBreakout(int? color) => color is 3 or 4;

	private sealed class DailySessionState
	{
		public DateTime Date { get; set; }
		public decimal? Session1High { get; set; }
		public decimal? Session1Low { get; set; }
		public decimal? Session2High { get; set; }
		public decimal? Session2Low { get; set; }
		public bool Session1Completed { get; set; }
		public bool Session2Completed { get; set; }
		public bool EntryTaken { get; set; }
	}
}