在 GitHub 上查看
Open Close 策略 (ID 3996)
概述
本策略对应 MetaTrader 4 智能交易系统 open_close.mq4。它仅关注一个品种,通过比较最近两根 K 线的开盘价与收盘价来寻找反转机会。当没有持仓时,策略逆势交易单根 K 线的过度波动;持仓期间,一旦出现相反信号或浮动亏损达到阈值便立即离场。
交易逻辑
入场条件
- 只有在上一根 K 线完全形成后才会交易(原始代码中的
Volume[0] == 1 判断)。
- 做多:当前 K 线开盘价高于上一根开盘价,且收盘价低于上一根收盘价,按市价买入设定的手数。
- 做空:当前 K 线开盘价低于上一根开盘价,且收盘价高于上一根收盘价,按市价卖出设定的手数。
任意时刻仅允许一笔净头寸,未平仓期间忽略新的信号。
离场条件
- 风险保护:根据平均持仓价计算浮动盈亏。当浮亏超过
MaximumRisk × Portfolio.CurrentValue 时,立即平仓。原版使用的 AccountMargin 在本实现中由账户市值的最佳可用估计替代。
- 形态反转:
- 多头:若下一根 K 线继续下跌(
open < 前一根 open 且 close < 前一根 close),立即平仓。
- 空头:若下一根 K 线继续上涨(
open > 前一根 open 且 close > 前一根 close),立即平仓。
仓位管理
- 订单基础手数由
MaximumRisk 决定:账户权益乘以风险系数再除以 1000,与原始公式 AccountFreeMargin * MaximumRisk / 1000 等效。
- 当无法获得账户数据时,退回到
InitialVolume 参数。
- 连续两笔以上亏损后,手数按
volume × losses / DecreaseFactor 减少,复刻 MQL 中遍历历史订单的逻辑。
- 强制最小手数为
0.1,随后根据交易品种的成交量步长和交易所限制进行对齐。
参数
| 名称 |
类型 |
默认值 |
说明 |
InitialVolume |
decimal |
0.1 |
当账户数据不可用时的备用手数。 |
MaximumRisk |
decimal |
0.3 |
控制下单手数与允许浮亏上限的账户资金比例。 |
DecreaseFactor |
decimal |
100 |
连续亏损后减少手数的衰减系数。 |
CandleType |
DataType |
15 分钟 |
用于分析形态的 K 线类型。 |
实现细节
- 订阅所选时间周期的 K 线,并仅在 K 线完结时运行逻辑,对应原策略的
Volume[0] > 1 约束。
- 由于 StockSharp 无法直接提供 MetaTrader 的
AccountProfit 和 AccountMargin,浮动盈亏通过当前仓位和最近收盘价估算。
- 连续亏损统计依赖成交回报,因此
DecreaseFactor 的行为与原始历史遍历保持一致。
- 手数会根据
Security.VolumeStep、MinVolume、MaxVolume 自动调整,避免违反交易所规则。
- 若可用图表区域,策略会绘制 K 线与自身成交,方便回测与可视化调试。
使用建议
- 选择与原 MetaTrader 策略相同的 K 线周期进行对比测试。
- 通过调节
MaximumRisk 与 DecreaseFactor 控制下单节奏与风险敞口。
- 该策略为逆势思路,更适合存在单根 K 线超买/超卖并伴随快速回落的市场环境。
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>
/// Contrarian pattern strategy converted from the MetaTrader expert "open_close".
/// Evaluates relationships between consecutive candle opens and closes.
/// Buys when a bearish candle opens above the previous open (fading the move),
/// and sells when a bullish candle opens below the previous open.
/// </summary>
public class OpenCloseStrategy : Strategy
{
private readonly StrategyParam<decimal> _stopLossPoints;
private readonly StrategyParam<decimal> _takeProfitPoints;
private readonly StrategyParam<DataType> _candleType;
private ExponentialMovingAverage _ema;
private bool _hasPreviousCandle;
private decimal _previousOpen;
private decimal _previousClose;
public OpenCloseStrategy()
{
_stopLossPoints = Param(nameof(StopLossPoints), 500m)
.SetNotNegative()
.SetDisplay("Stop Loss", "Stop loss in absolute points", "Risk");
_takeProfitPoints = Param(nameof(TakeProfitPoints), 500m)
.SetNotNegative()
.SetDisplay("Take Profit", "Take profit in absolute points", "Risk");
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
.SetDisplay("Candle Type", "Time-frame used to evaluate the open/close pattern.", "Data");
Volume = 1;
}
/// <summary>
/// Stop loss distance in absolute points.
/// </summary>
public decimal StopLossPoints
{
get => _stopLossPoints.Value;
set => _stopLossPoints.Value = value;
}
/// <summary>
/// Take profit distance in absolute points.
/// </summary>
public decimal TakeProfitPoints
{
get => _takeProfitPoints.Value;
set => _takeProfitPoints.Value = value;
}
/// <summary>
/// Candle series used to evaluate the pattern.
/// </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();
_hasPreviousCandle = false;
_previousOpen = 0m;
_previousClose = 0m;
_ema = null;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
_ema = new ExponentialMovingAverage { Length = 20 };
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(_ema, ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, _ema);
DrawOwnTrades(area);
}
var tp = TakeProfitPoints > 0 ? new Unit(TakeProfitPoints, UnitTypes.Absolute) : null;
var sl = StopLossPoints > 0 ? new Unit(StopLossPoints, UnitTypes.Absolute) : null;
if (tp != null || sl != null)
StartProtection(tp, sl);
base.OnStarted2(time);
}
private void ProcessCandle(ICandleMessage candle, decimal emaValue)
{
if (candle.State != CandleStates.Finished)
return;
var open = candle.OpenPrice;
var close = candle.ClosePrice;
if (!_hasPreviousCandle)
{
_previousOpen = open;
_previousClose = close;
_hasPreviousCandle = true;
return;
}
// Exit logic
if (Position > 0)
{
// Close long on bearish continuation
if (open < _previousOpen && close < _previousClose)
SellMarket(Position);
}
else if (Position < 0)
{
// Close short on bullish continuation
if (open > _previousOpen && close > _previousClose)
BuyMarket(Math.Abs(Position));
}
if (!IsFormedAndOnlineAndAllowTrading())
{
_previousOpen = open;
_previousClose = close;
return;
}
// Entry logic
if (Position == 0)
{
// Buy: fade a bearish candle that opened above the previous open
if (open > _previousOpen && close < _previousClose)
{
BuyMarket(Volume);
}
// Sell: fade a bullish candle that opened below the previous open
else if (open < _previousOpen && close > _previousClose)
{
SellMarket(Volume);
}
}
_previousOpen = open;
_previousClose = close;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Math
from StockSharp.Messages import DataType, CandleStates, UnitTypes, Unit
from StockSharp.Algo.Indicators import ExponentialMovingAverage
from StockSharp.Algo.Strategies import Strategy
class open_close_strategy(Strategy):
def __init__(self):
super(open_close_strategy, self).__init__()
self._sl_points = self.Param("StopLossPoints", 500.0).SetNotNegative().SetDisplay("Stop Loss", "Stop loss in absolute points", "Risk")
self._tp_points = self.Param("TakeProfitPoints", 500.0).SetNotNegative().SetDisplay("Take Profit", "Take profit in absolute points", "Risk")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4))).SetDisplay("Candle Type", "Time-frame for open/close pattern.", "Data")
self.Volume = 1
@property
def CandleType(self): return self._candle_type.Value
@CandleType.setter
def CandleType(self, value): self._candle_type.Value = value
def OnReseted(self):
super(open_close_strategy, self).OnReseted()
self._has_prev = False
self._prev_open = 0
self._prev_close = 0
def OnStarted2(self, time):
super(open_close_strategy, self).OnStarted2(time)
self._has_prev = False
self._prev_open = 0
self._prev_close = 0
ema = ExponentialMovingAverage()
ema.Length = 20
sub = self.SubscribeCandles(self.CandleType)
sub.Bind(ema, self.OnProcess).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, sub)
self.DrawIndicator(area, ema)
self.DrawOwnTrades(area)
tp = Unit(self._tp_points.Value, UnitTypes.Absolute) if self._tp_points.Value > 0 else None
sl = Unit(self._sl_points.Value, UnitTypes.Absolute) if self._sl_points.Value > 0 else None
if tp is not None or sl is not None:
self.StartProtection(tp, sl)
def OnProcess(self, candle, ema_val):
if candle.State != CandleStates.Finished:
return
open_price = candle.OpenPrice
close = candle.ClosePrice
if not self._has_prev:
self._prev_open = open_price
self._prev_close = close
self._has_prev = True
return
# Exit logic
if self.Position > 0:
if open_price < self._prev_open and close < self._prev_close:
self.SellMarket(self.Position)
elif self.Position < 0:
if open_price > self._prev_open and close > self._prev_close:
self.BuyMarket(Math.Abs(self.Position))
# Entry logic
if self.Position == 0:
if open_price > self._prev_open and close < self._prev_close:
self.BuyMarket(self.Volume)
elif open_price < self._prev_open and close > self._prev_close:
self.SellMarket(self.Volume)
self._prev_open = open_price
self._prev_close = close
def CreateClone(self):
return open_close_strategy()