重心蜡烛策略
该策略使用 StockSharp 高级 API 复刻 MetaTrader 专家顾问 “Exp_CenterOfGravityCandle”。指标对原始蜡烛的开、高、低、收价格分别应用 John Ehlers 的重心算法,并通过可配置的移动平均线进行平滑,构建出三色的合成蜡烛。蜡烛颜色(多头、空头或中性)即为唯一的交易信号。
指标原理
- 仅在市场蜡烛完全收盘后才参与计算。
- 对每个价格分量(开盘价、高点、低点、收盘价)分别计算周期为 Period 的简单移动平均线和线性加权移动平均线。
- 将两条均线的乘积除以品种的最小跳动点,并按 Ma Method 和 Smooth Period 指定的方式进行二次平滑。
- 为保持与 MetaTrader 指标一致,会强制让合成最高价与最低价覆盖合成开收盘价,从而保证蜡烛实体完整。
- 合成开盘价低于合成收盘价视为多头蜡烛(颜色 2),高于收盘价视为空头蜡烛(颜色 0),两者相等为中性蜡烛(颜色 1)。
交易规则
- 策略维护最近若干根合成蜡烛的颜色,并观察由 Signal Bar 指定的那一根(默认上一根已完成的蜡烛)。
- 当观测蜡烛从非多头变为多头时:
- 若 Enable Sell Close 为
true,关闭当前空头仓位。 - 若 Enable Buy Open 为
true,按计算手数开多仓。
- 若 Enable Sell Close 为
- 当观测蜡烛从非空头变为空头时:
- 若 Enable Buy Close 为
true,关闭当前多头仓位。 - 若 Enable Sell Open 为
true,按计算手数开空仓。
- 若 Enable Buy Close 为
- 入场手数依据 Money Management 与 Margin Mode 计算。Money Management 为负值时视为固定手数。选择亏损基准模式时,会结合 Stop Loss 距离估算单次风险。
- 调用
StartProtection,根据 Stop Loss 与 Take Profit(以价格跳动计算)自动挂出止损/止盈单。
参数
- Money Management – 决定下单手数的资金占比,负值代表直接指定手数,可参与优化。
- Margin Mode – 对资金占比的解释方式(权益、余额、亏损风险或固定手数)。
- Stop Loss – 止损距离(按价格跳动),用于仓位保护与风险计算。
- Take Profit – 止盈距离(按价格跳动),由
StartProtection执行。 - Open Long / Open Short – 是否允许在信号出现时开多/开空。
- Close Long / Close Short – 是否允许在反向信号出现时平多/平空。
- Candle Type – 用于计算指标的蜡烛周期。
- Center of Gravity Period – 简单与线性加权均线的基础周期,可优化。
- Smoothing Period – 合成蜡烛平滑阶段的周期,可优化。
- Smoothing Method – 平滑所用的移动平均类型(SMA、EMA、SMMA、LWMA)。
- Signal Bar – 用于产生信号的合成蜡烛索引(0 = 当前,1 = 上一根)。
说明
- 指标在 C# 中完整实现,复刻 MetaTrader 算法,无需额外的历史缓冲区。
- 手数计算依赖 StockSharp 的投资组合数据,与 MetaTrader 的结果可能存在细微差异。
- 策略仅处理完全收盘的蜡烛,不在未完成的柱子上交易。
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>
/// Strategy that trades synthetic candles generated by Center of Gravity calculation.
/// Uses SMA and LWMA products smoothed to create synthetic OHLC, then trades color changes.
/// </summary>
public class CenterOfGravityCandleStrategy : Strategy
{
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _period;
private readonly StrategyParam<int> _smoothPeriod;
// Internal indicators for computing synthetic candle
private SimpleMovingAverage _openSma;
private SimpleMovingAverage _highSma;
private SimpleMovingAverage _lowSma;
private SimpleMovingAverage _closeSma;
private WeightedMovingAverage _openLwma;
private WeightedMovingAverage _highLwma;
private WeightedMovingAverage _lowLwma;
private WeightedMovingAverage _closeLwma;
private SimpleMovingAverage _openSmooth;
private SimpleMovingAverage _highSmooth;
private SimpleMovingAverage _lowSmooth;
private SimpleMovingAverage _closeSmooth;
private int _prevColor; // 0=neutral, 1=bullish, -1=bearish
private bool _hasPrev;
public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
public int Period { get => _period.Value; set => _period.Value = value; }
public int SmoothPeriod { get => _smoothPeriod.Value; set => _smoothPeriod.Value = value; }
public CenterOfGravityCandleStrategy()
{
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Timeframe", "General");
_period = Param(nameof(Period), 10)
.SetGreaterThanZero()
.SetDisplay("CoG Period", "Center of gravity period", "Indicator");
_smoothPeriod = Param(nameof(SmoothPeriod), 1)
.SetGreaterThanZero()
.SetDisplay("Smooth Period", "Smoothing period", "Indicator");
}
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_prevColor = 0;
_hasPrev = false;
_openSma = null;
_highSma = null;
_lowSma = null;
_closeSma = null;
_openLwma = null;
_highLwma = null;
_lowLwma = null;
_closeLwma = null;
_openSmooth = null;
_highSmooth = null;
_lowSmooth = null;
_closeSmooth = null;
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
var len = Period;
var sLen = SmoothPeriod;
_openSma = new SMA { Length = len };
_highSma = new SMA { Length = len };
_lowSma = new SMA { Length = len };
_closeSma = new SMA { Length = len };
_openLwma = new WeightedMovingAverage { Length = len };
_highLwma = new WeightedMovingAverage { Length = len };
_lowLwma = new WeightedMovingAverage { Length = len };
_closeLwma = new WeightedMovingAverage { Length = len };
_openSmooth = new SMA { Length = sLen };
_highSmooth = new SMA { Length = sLen };
_lowSmooth = new SMA { Length = sLen };
_closeSmooth = new SMA { Length = sLen };
_prevColor = 0;
_hasPrev = false;
var sub = SubscribeCandles(CandleType);
sub.Bind(ProcessCandle).Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, sub);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
var t = candle.OpenTime;
var point = Security?.PriceStep ?? 1m;
if (point <= 0m) point = 1m;
// Compute SMA and LWMA for each OHLC component
var oSma = _openSma.Process(candle.OpenPrice, t, true);
var oLwma = _openLwma.Process(candle.OpenPrice, t, true);
var hSma = _highSma.Process(candle.HighPrice, t, true);
var hLwma = _highLwma.Process(candle.HighPrice, t, true);
var lSma = _lowSma.Process(candle.LowPrice, t, true);
var lLwma = _lowLwma.Process(candle.LowPrice, t, true);
var cSma = _closeSma.Process(candle.ClosePrice, t, true);
var cLwma = _closeLwma.Process(candle.ClosePrice, t, true);
if (!_openSma.IsFormed || !_openLwma.IsFormed ||
!_highSma.IsFormed || !_highLwma.IsFormed ||
!_lowSma.IsFormed || !_lowLwma.IsFormed ||
!_closeSma.IsFormed || !_closeLwma.IsFormed)
return;
// Use average of SMA and LWMA instead of product (avoids overflow with high prices)
var openProd = (oSma.ToDecimal() + oLwma.ToDecimal()) / 2m;
var highProd = (hSma.ToDecimal() + hLwma.ToDecimal()) / 2m;
var lowProd = (lSma.ToDecimal() + lLwma.ToDecimal()) / 2m;
var closeProd = (cSma.ToDecimal() + cLwma.ToDecimal()) / 2m;
// Smooth
var oSmooth = _openSmooth.Process(openProd, t, true);
var hSmooth = _highSmooth.Process(highProd, t, true);
var lSmooth = _lowSmooth.Process(lowProd, t, true);
var cSmooth = _closeSmooth.Process(closeProd, t, true);
if (!_openSmooth.IsFormed || !_highSmooth.IsFormed ||
!_lowSmooth.IsFormed || !_closeSmooth.IsFormed)
return;
var openVal = oSmooth.ToDecimal();
var closeVal = cSmooth.ToDecimal();
// Determine color
int color;
if (openVal < closeVal)
color = 1; // bullish
else if (openVal > closeVal)
color = -1; // bearish
else
color = 0; // neutral
if (_hasPrev)
{
// Bullish color change
if (color == 1 && _prevColor != 1 && Position <= 0)
BuyMarket();
// Bearish color change
else if (color == -1 && _prevColor != -1 && Position >= 0)
SellMarket();
}
_prevColor = color;
_hasPrev = true;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Indicators import SimpleMovingAverage, WeightedMovingAverage
from StockSharp.Algo.Strategies import Strategy
from indicator_extensions import *
class center_of_gravity_candle_strategy(Strategy):
"""
Trades synthetic candles generated by Center of Gravity calculation.
Uses SMA and LWMA averages smoothed to create synthetic OHLC, then trades color changes.
"""
def __init__(self):
super(center_of_gravity_candle_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))) \
.SetDisplay("Candle Type", "Timeframe", "General")
self._period = self.Param("Period", 10) \
.SetDisplay("CoG Period", "Center of gravity period", "Indicator")
self._smooth_period = self.Param("SmoothPeriod", 1) \
.SetDisplay("Smooth Period", "Smoothing period", "Indicator")
self._prev_color = 0
self._has_prev = False
self._open_sma = None
self._high_sma = None
self._low_sma = None
self._close_sma = None
self._open_lwma = None
self._high_lwma = None
self._low_lwma = None
self._close_lwma = None
self._open_smooth = None
self._high_smooth = None
self._low_smooth = None
self._close_smooth = None
@property
def candle_type(self):
return self._candle_type.Value
def OnReseted(self):
super(center_of_gravity_candle_strategy, self).OnReseted()
self._prev_color = 0
self._has_prev = False
self._open_sma = None
self._high_sma = None
self._low_sma = None
self._close_sma = None
self._open_lwma = None
self._high_lwma = None
self._low_lwma = None
self._close_lwma = None
self._open_smooth = None
self._high_smooth = None
self._low_smooth = None
self._close_smooth = None
def OnStarted2(self, time):
super(center_of_gravity_candle_strategy, self).OnStarted2(time)
length = self._period.Value
s_len = self._smooth_period.Value
self._open_sma = SimpleMovingAverage()
self._open_sma.Length = length
self._high_sma = SimpleMovingAverage()
self._high_sma.Length = length
self._low_sma = SimpleMovingAverage()
self._low_sma.Length = length
self._close_sma = SimpleMovingAverage()
self._close_sma.Length = length
self._open_lwma = WeightedMovingAverage()
self._open_lwma.Length = length
self._high_lwma = WeightedMovingAverage()
self._high_lwma.Length = length
self._low_lwma = WeightedMovingAverage()
self._low_lwma.Length = length
self._close_lwma = WeightedMovingAverage()
self._close_lwma.Length = length
self._open_smooth = SimpleMovingAverage()
self._open_smooth.Length = s_len
self._high_smooth = SimpleMovingAverage()
self._high_smooth.Length = s_len
self._low_smooth = SimpleMovingAverage()
self._low_smooth.Length = s_len
self._close_smooth = SimpleMovingAverage()
self._close_smooth.Length = s_len
self._prev_color = 0
self._has_prev = False
subscription = self.SubscribeCandles(self.candle_type)
subscription.Bind(self.on_process).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawOwnTrades(area)
def on_process(self, candle):
if candle.State != CandleStates.Finished:
return
t = candle.OpenTime
def _proc(ind, val):
return process_float(ind, val, t, True)
o_sma = _proc(self._open_sma, candle.OpenPrice)
o_lwma = _proc(self._open_lwma, candle.OpenPrice)
h_sma = _proc(self._high_sma, candle.HighPrice)
h_lwma = _proc(self._high_lwma, candle.HighPrice)
l_sma = _proc(self._low_sma, candle.LowPrice)
l_lwma = _proc(self._low_lwma, candle.LowPrice)
c_sma = _proc(self._close_sma, candle.ClosePrice)
c_lwma = _proc(self._close_lwma, candle.ClosePrice)
if (not self._open_sma.IsFormed or not self._open_lwma.IsFormed or
not self._high_sma.IsFormed or not self._high_lwma.IsFormed or
not self._low_sma.IsFormed or not self._low_lwma.IsFormed or
not self._close_sma.IsFormed or not self._close_lwma.IsFormed):
return
open_prod = (float(o_sma) + float(o_lwma)) / 2.0
high_prod = (float(h_sma) + float(h_lwma)) / 2.0
low_prod = (float(l_sma) + float(l_lwma)) / 2.0
close_prod = (float(c_sma) + float(c_lwma)) / 2.0
o_smooth = _proc(self._open_smooth, open_prod)
h_smooth = _proc(self._high_smooth, high_prod)
l_smooth = _proc(self._low_smooth, low_prod)
c_smooth = _proc(self._close_smooth, close_prod)
if (not self._open_smooth.IsFormed or not self._high_smooth.IsFormed or
not self._low_smooth.IsFormed or not self._close_smooth.IsFormed):
return
open_val = float(o_smooth)
close_val = float(c_smooth)
if open_val < close_val:
color = 1
elif open_val > close_val:
color = -1
else:
color = 0
if self._has_prev:
if color == 1 and self._prev_color != 1 and self.Position <= 0:
self.BuyMarket()
elif color == -1 and self._prev_color != -1 and self.Position >= 0:
self.SellMarket()
self._prev_color = color
self._has_prev = True
def CreateClone(self):
return center_of_gravity_candle_strategy()