Kyrie Crossover 策略
该策略基于指数移动平均线(EMA)交叉进行交易。当短期 EMA 上穿长期 EMA 时做多,当短期 EMA 下穿长期 EMA 时做空。止损根据入场价的百分比设置。
参数
- K线类型
- 短期 EMA 周期
- 长期 EMA 周期
- 风险百分比
using System;
using System.Collections.Generic;
using Ecng.Common;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// EMA crossover strategy with configurable stop-loss.
/// Buys when the short EMA crosses above the long EMA and sells short on the opposite crossover.
/// </summary>
public class KyrieCrossoverStrategy : Strategy
{
private readonly StrategyParam<int> _shortEmaPeriod;
private readonly StrategyParam<int> _longEmaPeriod;
private readonly StrategyParam<decimal> _riskPercent;
private readonly StrategyParam<int> _maxEntries;
private readonly StrategyParam<int> _cooldownBars;
private readonly StrategyParam<DataType> _candleType;
private decimal _entryPrice;
private bool _isLong;
private int _entriesExecuted;
private int _barsSinceSignal;
/// <summary>
/// Short EMA period.
/// </summary>
public int ShortEmaPeriod
{
get => _shortEmaPeriod.Value;
set => _shortEmaPeriod.Value = value;
}
/// <summary>
/// Long EMA period.
/// </summary>
public int LongEmaPeriod
{
get => _longEmaPeriod.Value;
set => _longEmaPeriod.Value = value;
}
/// <summary>
/// Stop-loss percentage.
/// </summary>
public decimal RiskPercent
{
get => _riskPercent.Value;
set => _riskPercent.Value = value;
}
/// <summary>
/// Type of candles used for calculations.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Maximum entries per run.
/// </summary>
public int MaxEntries
{
get => _maxEntries.Value;
set => _maxEntries.Value = value;
}
/// <summary>
/// Minimum bars between orders.
/// </summary>
public int CooldownBars
{
get => _cooldownBars.Value;
set => _cooldownBars.Value = value;
}
/// <summary>
/// Initializes a new instance of the strategy.
/// </summary>
public KyrieCrossoverStrategy()
{
_shortEmaPeriod = Param(nameof(ShortEmaPeriod), 11)
.SetGreaterThanZero()
.SetDisplay("Short EMA Period", "Period of the short EMA", "EMA Settings")
.SetOptimize(5, 20, 1);
_longEmaPeriod = Param(nameof(LongEmaPeriod), 323)
.SetGreaterThanZero()
.SetDisplay("Long EMA Period", "Period of the long EMA", "EMA Settings")
.SetOptimize(100, 500, 10);
_riskPercent = Param(nameof(RiskPercent), 1.0m)
.SetGreaterThanZero()
.SetDisplay("Risk %", "Stop loss percentage from entry price", "Risk Management")
.SetOptimize(0.5m, 5.0m, 0.5m);
_maxEntries = Param(nameof(MaxEntries), 45)
.SetGreaterThanZero()
.SetDisplay("Max Entries", "Maximum entries per run", "Risk Management");
_cooldownBars = Param(nameof(CooldownBars), 240)
.SetGreaterThanZero()
.SetDisplay("Cooldown Bars", "Minimum bars between orders", "Risk Management");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(1).TimeFrame())
.SetDisplay("Candle Type", "Type of candles to use", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_entryPrice = 0m;
_isLong = false;
_entriesExecuted = 0;
_barsSinceSignal = 0;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_entriesExecuted = 0;
_barsSinceSignal = CooldownBars;
var shortEma = new EMA { Length = ShortEmaPeriod };
var longEma = new EMA { Length = LongEmaPeriod };
var subscription = SubscribeCandles(CandleType);
var prevShort = 0m;
var prevLong = 0m;
var wasShortBelowLong = false;
var initialized = false;
subscription
.Bind(shortEma, longEma, (candle, shortValue, longValue) =>
{
if (candle.State != CandleStates.Finished)
return;
_barsSinceSignal++;
if (!IsFormedAndOnlineAndAllowTrading())
return;
if (!initialized && shortEma.IsFormed && longEma.IsFormed)
{
prevShort = shortValue;
prevLong = longValue;
wasShortBelowLong = shortValue < longValue;
initialized = true;
return;
}
if (!initialized)
return;
var isShortBelowLong = shortValue < longValue;
if (wasShortBelowLong != isShortBelowLong && _entriesExecuted < MaxEntries && _barsSinceSignal >= CooldownBars)
{
if (!isShortBelowLong && Position <= 0)
{
_entryPrice = candle.ClosePrice;
_isLong = true;
BuyMarket(Volume + Math.Abs(Position));
_entriesExecuted++;
_barsSinceSignal = 0;
}
else if (isShortBelowLong && Position >= 0)
{
_entryPrice = candle.ClosePrice;
_isLong = false;
SellMarket(Volume + Math.Abs(Position));
_entriesExecuted++;
_barsSinceSignal = 0;
}
wasShortBelowLong = isShortBelowLong;
}
if (Position != 0 && _entryPrice != 0)
CheckStopLoss(candle.ClosePrice);
prevShort = shortValue;
prevLong = longValue;
})
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, shortEma);
DrawIndicator(area, longEma);
DrawOwnTrades(area);
}
}
private void CheckStopLoss(decimal currentPrice)
{
var stopLossThreshold = _riskPercent.Value / 100m;
if (_isLong && Position > 0)
{
var stopPrice = _entryPrice * (1m - stopLossThreshold);
if (currentPrice <= stopPrice)
{
SellMarket(Math.Abs(Position));
_entryPrice = 0m;
_barsSinceSignal = 0;
}
}
else if (!_isLong && Position < 0)
{
var stopPrice = _entryPrice * (1m + stopLossThreshold);
if (currentPrice >= stopPrice)
{
BuyMarket(Math.Abs(Position));
_entryPrice = 0m;
_barsSinceSignal = 0;
}
}
}
}
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 kyrie_crossover_strategy(Strategy):
def __init__(self):
super(kyrie_crossover_strategy, self).__init__()
self._short_ema_period = self.Param("ShortEmaPeriod", 11) \
.SetGreaterThanZero() \
.SetDisplay("Short EMA Period", "Period of the short EMA", "EMA Settings")
self._long_ema_period = self.Param("LongEmaPeriod", 323) \
.SetGreaterThanZero() \
.SetDisplay("Long EMA Period", "Period of the long EMA", "EMA Settings")
self._risk_percent = self.Param("RiskPercent", 1.0) \
.SetGreaterThanZero() \
.SetDisplay("Risk Pct", "Stop loss percentage from entry price", "Risk Management")
self._max_entries = self.Param("MaxEntries", 45) \
.SetGreaterThanZero() \
.SetDisplay("Max Entries", "Maximum entries per run", "Risk Management")
self._cooldown_bars = self.Param("CooldownBars", 240) \
.SetGreaterThanZero() \
.SetDisplay("Cooldown Bars", "Minimum bars between orders", "Risk Management")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(1))) \
.SetDisplay("Candle Type", "Type of candles to use", "General")
self._entry_price = 0.0
self._is_long = False
self._entries_executed = 0
self._bars_since_signal = 0
self._prev_short = 0.0
self._prev_long = 0.0
self._was_short_below_long = False
self._initialized = False
@property
def candle_type(self):
return self._candle_type.Value
@candle_type.setter
def candle_type(self, value):
self._candle_type.Value = value
def OnReseted(self):
super(kyrie_crossover_strategy, self).OnReseted()
self._entry_price = 0.0
self._is_long = False
self._entries_executed = 0
self._bars_since_signal = 0
self._prev_short = 0.0
self._prev_long = 0.0
self._was_short_below_long = False
self._initialized = False
def OnStarted2(self, time):
super(kyrie_crossover_strategy, self).OnStarted2(time)
self._entries_executed = 0
self._bars_since_signal = self._cooldown_bars.Value
short_ema = ExponentialMovingAverage()
short_ema.Length = self._short_ema_period.Value
long_ema = ExponentialMovingAverage()
long_ema.Length = self._long_ema_period.Value
subscription = self.SubscribeCandles(self.candle_type)
subscription.Bind(short_ema, long_ema, self.OnProcess).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, short_ema)
self.DrawIndicator(area, long_ema)
self.DrawOwnTrades(area)
def OnProcess(self, candle, short_val, long_val):
if candle.State != CandleStates.Finished:
return
self._bars_since_signal += 1
sv = float(short_val)
lv = float(long_val)
close = float(candle.ClosePrice)
if not self._initialized:
self._prev_short = sv
self._prev_long = lv
self._was_short_below_long = sv < lv
self._initialized = True
return
is_short_below_long = sv < lv
if self._was_short_below_long != is_short_below_long and self._entries_executed < self._max_entries.Value and self._bars_since_signal >= self._cooldown_bars.Value:
if not is_short_below_long and self.Position <= 0:
self._entry_price = close
self._is_long = True
self.BuyMarket()
self._entries_executed += 1
self._bars_since_signal = 0
elif is_short_below_long and self.Position >= 0:
self._entry_price = close
self._is_long = False
self.SellMarket()
self._entries_executed += 1
self._bars_since_signal = 0
self._was_short_below_long = is_short_below_long
if self.Position != 0 and self._entry_price != 0.0:
self._check_stop_loss(close)
self._prev_short = sv
self._prev_long = lv
def _check_stop_loss(self, current_price):
threshold = float(self._risk_percent.Value) / 100.0
if self._is_long and self.Position > 0:
stop_price = self._entry_price * (1.0 - threshold)
if current_price <= stop_price:
self.SellMarket()
self._entry_price = 0.0
self._bars_since_signal = 0
elif not self._is_long and self.Position < 0:
stop_price = self._entry_price * (1.0 + threshold)
if current_price >= stop_price:
self.BuyMarket()
self._entry_price = 0.0
self._bars_since_signal = 0
def CreateClone(self):
return kyrie_crossover_strategy()