Monthly Returns 策略
该策略利用最近的枢轴高点和低点突破进行交易,并计算策略权益的复利月度和年度收益。
详情
- 入场条件:价格突破最新枢轴高点时做多,突破最新枢轴低点时做空。
- 多空方向:双向。
- 出场条件:出现反向信号时反转持仓。
- 止损:无。
- 默认值:
LeftBars= 2RightBars= 1CandleType= TimeSpan.FromDays(1)
- 过滤器:
- 分类:Breakout
- 方向:Long & Short
- 指标:无
- 止损:无
- 复杂度:基础
- 时间框架:日线
- 季节性:无
- 神经网络:无
- 背离:无
- 风险等级:中等
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 calculating monthly and yearly returns using pivot breakouts.
/// </summary>
public class MonthlyReturnsStrategy : Strategy
{
private readonly StrategyParam<int> _leftBars;
private readonly StrategyParam<int> _rightBars;
private readonly StrategyParam<int> _cooldownBars;
private readonly StrategyParam<decimal> _breakoutOffsetPercent;
private readonly StrategyParam<DataType> _candleType;
private readonly List<decimal> _highBuffer = new();
private readonly List<decimal> _lowBuffer = new();
private decimal _hPrice;
private decimal _lPrice;
private bool _awaitLong;
private bool _awaitShort;
private int _barIndex;
private int _lastSignalBar = -1000000;
private decimal _prevEquity = 1m;
private decimal _curMonthReturn;
private decimal _curYearReturn;
private int _prevMonth = -1;
private int _prevYear = -1;
private readonly List<decimal> _monthReturns = new();
private readonly List<DateTime> _monthTimes = new();
private readonly List<decimal> _yearReturns = new();
private readonly List<int> _yearTimes = new();
/// <summary>
/// Number of bars to the left for pivot detection.
/// </summary>
public int LeftBars
{
get => _leftBars.Value;
set => _leftBars.Value = value;
}
/// <summary>
/// Number of bars to the right for pivot detection.
/// </summary>
public int RightBars
{
get => _rightBars.Value;
set => _rightBars.Value = value;
}
/// <summary>
/// Candle type used by the strategy.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Minimum number of finished candles between signals.
/// </summary>
public int CooldownBars
{
get => _cooldownBars.Value;
set => _cooldownBars.Value = value;
}
/// <summary>
/// Minimum breakout offset in percents from pivot level.
/// </summary>
public decimal BreakoutOffsetPercent
{
get => _breakoutOffsetPercent.Value;
set => _breakoutOffsetPercent.Value = value;
}
/// <summary>
/// Initialize <see cref="MonthlyReturnsStrategy"/>.
/// </summary>
public MonthlyReturnsStrategy()
{
_leftBars = Param(nameof(LeftBars), 6)
.SetDisplay("Left Bars", "Bars to the left for pivots", "General")
.SetGreaterThanZero();
_rightBars = Param(nameof(RightBars), 3)
.SetDisplay("Right Bars", "Bars to the right for pivots", "General")
.SetGreaterThanZero();
_cooldownBars = Param(nameof(CooldownBars), 14)
.SetDisplay("Cooldown Bars", "Finished candles between entries", "General")
.SetGreaterThanZero();
_breakoutOffsetPercent = Param(nameof(BreakoutOffsetPercent), 0.10m)
.SetDisplay("Breakout Offset %", "Min breakout offset from pivot", "General")
.SetCanOptimize(true)
.SetGreaterThanZero();
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(10).TimeFrame())
.SetDisplay("Candle Type", "Type of candles", "General");
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
var dummyEma1 = new ExponentialMovingAverage { Length = 10 };
var dummyEma2 = new ExponentialMovingAverage { Length = 20 };
var subscription = SubscribeCandles(CandleType);
subscription.Bind(dummyEma1, dummyEma2, ProcessCandle).Start();
}
private void ProcessCandle(ICandleMessage candle, decimal d1, decimal d2)
{
if (candle.State != CandleStates.Finished)
return;
_barIndex++;
_highBuffer.Add(candle.HighPrice);
_lowBuffer.Add(candle.LowPrice);
var size = LeftBars + RightBars + 1;
if (_highBuffer.Count > size)
_highBuffer.RemoveAt(0);
if (_lowBuffer.Count > size)
_lowBuffer.RemoveAt(0);
if (_highBuffer.Count == size)
{
var idx = _highBuffer.Count - RightBars - 1;
var candidate = _highBuffer[idx];
var isPivot = true;
for (var i = 0; i < _highBuffer.Count; i++)
{
if (i == idx)
continue;
if (_highBuffer[i] >= candidate)
{
isPivot = false;
break;
}
}
if (isPivot)
{
_hPrice = candidate;
_awaitLong = true;
}
}
if (_lowBuffer.Count == size)
{
var idx = _lowBuffer.Count - RightBars - 1;
var candidate = _lowBuffer[idx];
var isPivot = true;
for (var i = 0; i < _lowBuffer.Count; i++)
{
if (i == idx)
continue;
if (_lowBuffer[i] <= candidate)
{
isPivot = false;
break;
}
}
if (isPivot)
{
_lPrice = candidate;
_awaitShort = true;
}
}
var canSignal = _barIndex - _lastSignalBar >= CooldownBars;
var longTrigger = _hPrice * (1m + BreakoutOffsetPercent / 100m);
var shortTrigger = _lPrice * (1m - BreakoutOffsetPercent / 100m);
if (_awaitLong && canSignal && candle.HighPrice > longTrigger && Position <= 0)
{
BuyMarket();
_awaitLong = false;
_awaitShort = false;
_lastSignalBar = _barIndex;
}
if (_awaitShort && canSignal && candle.LowPrice < shortTrigger && Position >= 0)
{
SellMarket();
_awaitShort = false;
_awaitLong = false;
_lastSignalBar = _barIndex;
}
UpdateReturns(candle);
}
private void UpdateReturns(ICandleMessage candle)
{
var eq = 1m + PnL;
var barReturn = eq / _prevEquity - 1m;
_prevEquity = eq;
var month = candle.CloseTime.Month;
var year = candle.CloseTime.Year;
if (_prevMonth == -1)
{
_prevMonth = month;
_prevYear = year;
}
if (month != _prevMonth)
{
_monthReturns.Add(_curMonthReturn);
_monthTimes.Add(new DateTime(_prevYear, _prevMonth, 1));
_curMonthReturn = 0m;
_prevMonth = month;
}
if (year != _prevYear)
{
_yearReturns.Add(_curYearReturn);
_yearTimes.Add(_prevYear);
_curYearReturn = 0m;
_prevYear = year;
}
_curMonthReturn = (1m + _curMonthReturn) * (1m + barReturn) - 1m;
_curYearReturn = (1m + _curYearReturn) * (1m + barReturn) - 1m;
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_highBuffer.Clear();
_lowBuffer.Clear();
_monthReturns.Clear();
_monthTimes.Clear();
_yearReturns.Clear();
_yearTimes.Clear();
_hPrice = 0m;
_lPrice = 0m;
_awaitLong = false;
_awaitShort = false;
_barIndex = 0;
_lastSignalBar = -1000000;
_prevEquity = 1m;
_curMonthReturn = 0m;
_curYearReturn = 0m;
_prevMonth = -1;
_prevYear = -1;
}
}
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 ExponentialMovingAverage
from StockSharp.Algo.Strategies import Strategy
class monthly_returns_strategy(Strategy):
"""
Monthly returns: pivot breakout strategy with high/low pivot detection.
"""
def __init__(self):
super(monthly_returns_strategy, self).__init__()
self._left_bars = self.Param("LeftBars", 6).SetDisplay("Left Bars", "Left bars for pivots", "General")
self._right_bars = self.Param("RightBars", 3).SetDisplay("Right Bars", "Right bars for pivots", "General")
self._cooldown_bars = self.Param("CooldownBars", 14).SetDisplay("Cooldown", "Min bars between entries", "General")
self._breakout_pct = self.Param("BreakoutOffsetPercent", 0.10).SetDisplay("Breakout %", "Min breakout offset", "General")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(10))).SetDisplay("Candle Type", "Candles", "General")
self._high_buf = []
self._low_buf = []
self._h_price = 0.0
self._l_price = 0.0
self._await_long = False
self._await_short = False
self._bar_index = 0
self._last_signal_bar = -1000000
@property
def candle_type(self):
return self._candle_type.Value
def OnReseted(self):
super(monthly_returns_strategy, self).OnReseted()
self._high_buf = []
self._low_buf = []
self._h_price = 0.0
self._l_price = 0.0
self._await_long = False
self._await_short = False
self._bar_index = 0
self._last_signal_bar = -1000000
def OnStarted2(self, time):
super(monthly_returns_strategy, self).OnStarted2(time)
ema1 = ExponentialMovingAverage()
ema1.Length = 10
ema2 = ExponentialMovingAverage()
ema2.Length = 20
subscription = self.SubscribeCandles(self.candle_type)
subscription.Bind(ema1, ema2, self._process_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawOwnTrades(area)
def _process_candle(self, candle, d1, d2):
if candle.State != CandleStates.Finished:
return
self._bar_index += 1
high = float(candle.HighPrice)
low = float(candle.LowPrice)
self._high_buf.append(high)
self._low_buf.append(low)
left = self._left_bars.Value
right = self._right_bars.Value
size = left + right + 1
if len(self._high_buf) > size:
self._high_buf = self._high_buf[-size:]
if len(self._low_buf) > size:
self._low_buf = self._low_buf[-size:]
if len(self._high_buf) == size:
idx = len(self._high_buf) - right - 1
candidate = self._high_buf[idx]
is_pivot = True
for i in range(len(self._high_buf)):
if i == idx:
continue
if self._high_buf[i] >= candidate:
is_pivot = False
break
if is_pivot:
self._h_price = candidate
self._await_long = True
if len(self._low_buf) == size:
idx = len(self._low_buf) - right - 1
candidate = self._low_buf[idx]
is_pivot = True
for i in range(len(self._low_buf)):
if i == idx:
continue
if self._low_buf[i] <= candidate:
is_pivot = False
break
if is_pivot:
self._l_price = candidate
self._await_short = True
can_signal = self._bar_index - self._last_signal_bar >= self._cooldown_bars.Value
bo_pct = float(self._breakout_pct.Value)
long_trigger = self._h_price * (1.0 + bo_pct / 100.0)
short_trigger = self._l_price * (1.0 - bo_pct / 100.0)
if self._await_long and can_signal and high > long_trigger and self.Position <= 0:
self.BuyMarket()
self._await_long = False
self._await_short = False
self._last_signal_bar = self._bar_index
if self._await_short and can_signal and low < short_trigger and self.Position >= 0:
self.SellMarket()
self._await_short = False
self._await_long = False
self._last_signal_bar = self._bar_index
def CreateClone(self):
return monthly_returns_strategy()