在 GitHub 上查看

OSF 逆势策略

该策略复刻 Open Source Forex 的“超买/超卖”逆势专家。 通过一个 RSI 指标来近似原始振荡器,并把与 50 均衡水平的偏离视为方向与仓位规模信号。 所有交易都在完成的K线收盘时触发,并以固定点数的止盈价离场。

交易规则

  • 数据:使用参数 CandleType 定义的已完成K线。
  • 指标:周期为 RsiPeriod 的 RSI。原版专家对五个相同的 RSI 结果求平均, 因此此处单个 RSI 即可重现行为。
  • 信号逻辑
    • 当 RSI > 50 时认为市场超买,开空头仓位。
    • 当 RSI < 50 时认为市场超卖,开多头仓位。
    • 绝对偏差 |RSI − 50| 乘以 VolumePerPoint 得出下单数量。
  • 冷却:每次下单后等待 CooldownBars 根已完成K线后再评估下一次入场, 对应原代码中的柱状平滑。
  • 离场:下单后设置手动止盈,距离为 TakeProfitPoints × PriceStep。 策略不使用止损,与原始专家保持一致。
  • 反手:如果出现反向信号,会通过增加委托数量先平掉当前仓位再建立新仓。

参数

参数 说明
RsiPeriod 用于逼近 OSF 振荡器的 RSI 周期,默认 14。
VolumePerPoint RSI 每偏离 1 点所交易的数量,默认 0.01。
TakeProfitPoints 止盈距离(以合约最小价位单位计),默认 150。
CooldownBars 每次交易后的等待K线数量,默认 5。
CandleType 指标计算所使用的K线类型,默认 1 分钟。

备注

  • 若所选品种未设置 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>
/// Countertrend strategy based on the Open Source Forex oscillator.
/// </summary>
public class OsfCountertrendStrategy : Strategy
{
	private readonly StrategyParam<int> _rsiPeriod;
	private readonly StrategyParam<decimal> _volumePerPoint;
	private readonly StrategyParam<decimal> _takeProfitPoints;
	private readonly StrategyParam<int> _cooldownBars;
	private readonly StrategyParam<DataType> _candleType;

	private RelativeStrengthIndex _rsi;
	private int _cooldown;
	private decimal _longTarget;
	private decimal _shortTarget;

	/// <summary>
	/// RSI period used to approximate the original OSF oscillator.
	/// </summary>
	public int RsiPeriod
	{
		get => _rsiPeriod.Value;
		set => _rsiPeriod.Value = value;
	}

	/// <summary>
	/// Volume traded per RSI point away from equilibrium (50 level).
	/// </summary>
	public decimal VolumePerPoint
	{
		get => _volumePerPoint.Value;
		set => _volumePerPoint.Value = value;
	}

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

	/// <summary>
	/// Number of finished candles to wait before a new signal can trigger.
	/// </summary>
	public int CooldownBars
	{
		get => _cooldownBars.Value;
		set => _cooldownBars.Value = value;
	}

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

	/// <summary>
	/// Initializes a new instance of <see cref="OsfCountertrendStrategy"/>.
	/// </summary>
	public OsfCountertrendStrategy()
	{
		_rsiPeriod = Param(nameof(RsiPeriod), 14)
		.SetRange(2, 200)
		
		.SetDisplay("RSI Period", "RSI length used in oscillator", "General");

		_volumePerPoint = Param(nameof(VolumePerPoint), 0.01m)
		.SetRange(0.001m, 1m)
		
		.SetDisplay("Volume per Point", "Order volume per RSI point from 50", "Risk");

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 150m)
		.SetRange(0m, 1000m)
		
		.SetDisplay("Take Profit", "Distance to take profit in points", "Risk");

		_cooldownBars = Param(nameof(CooldownBars), 5)
		.SetRange(0, 50)
		
		.SetDisplay("Cooldown Bars", "Finished candles to wait after trading", "General");

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
		.SetDisplay("Candle Type", "Data series for processing", "General");
	}

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

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

		_rsi = null;
		_cooldown = 0;
		_longTarget = 0m;
		_shortTarget = 0m;
	}

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

		_rsi = new RelativeStrengthIndex
		{
			Length = RsiPeriod
		};

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

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

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

		if (!_rsi.IsFormed)
		return;

		// indicators already checked above

		// Track active positions for manual take-profit handling.
		if (Position > 0 && _longTarget > 0m && TakeProfitPoints > 0m)
		{
			if (candle.LowPrice <= _longTarget)
			{
				SellMarket();
				_longTarget = 0m;
			}
		}
		else if (Position < 0 && _shortTarget > 0m && TakeProfitPoints > 0m)
		{
			if (candle.HighPrice >= _shortTarget)
			{
				BuyMarket();
				_shortTarget = 0m;
			}
		}

		if (_cooldown > 0)
		{
			_cooldown--;
			return;
		}

		var diff = rsiValue - 50m;
		if (diff == 0m)
		return;

		var absDiff = Math.Abs(diff);
		var volume = absDiff * VolumePerPoint;
		if (volume <= 0m)
		return;

		var step = Security?.PriceStep ?? 1m;

		if (diff > 0m && Position <= 0m)
		{
			// RSI above 50: countertrend short trade sized by oscillator distance.
			var volumeToSell = volume + Math.Max(0m, Position);
			if (volumeToSell <= 0m)
			return;

			SellMarket();

			_shortTarget = TakeProfitPoints > 0m
			? candle.ClosePrice - step * TakeProfitPoints
			: 0m;
			_longTarget = 0m;
			_cooldown = CooldownBars;
		}
		else if (diff < 0m && Position >= 0m)
		{
			// RSI below 50: countertrend long trade sized by oscillator distance.
			var volumeToBuy = volume + Math.Max(0m, -Position);
			if (volumeToBuy <= 0m)
			return;

			BuyMarket();

			_longTarget = TakeProfitPoints > 0m
			? candle.ClosePrice + step * TakeProfitPoints
			: 0m;
			_shortTarget = 0m;
			_cooldown = CooldownBars;
		}
	}
}