Starter 2005 策略
概述
Starter 2005 Strategy 是对 MetaTrader 4 经典专家顾问 Starter.mq4(2005 年版)的 StockSharp 高阶 API 迁移。原始系统结合了 Laguerre 振荡器、指数移动平均(EMA)斜率过滤以及 CCI 确认。本移植在保留决策结构的同时,将资金管理和订单执行方式适配到 StockSharp:
- Laguerre RSI 代理重建了
iCustom("Laguerre")指标缓冲区,其输出在 0 与 1 之间摆动。 - 以 5 根 K 线为周期、作用于中间价
(High + Low) / 2的 EMA 提供了与 MT4 中相同的趋势斜率判定。 - 14 周期的 CCI 使用收盘价,复制了原代码中
Alpha变量的过滤效果。 LotsOptimized()的自适应手数逻辑被完整复刻,包括连续亏损后的减仓机制。- 持仓在 Laguerre 脱离极值区域或价格走出
Point * Stop的利润距离时平仓。
交易逻辑
- 指标初始化
- 通过四级 Laguerre 滤波重建 Laguerre RSI,
Gamma可配置。 - EMA 使用 5 周期并以
(High + Low) / 2为输入,完全对齐 MQL4 的PRICE_MEDIAN选项。 - CCI 默认 14 周期,
±5的阈值保持不变以最大限度贴近旧策略。
- 通过四级 Laguerre 滤波重建 Laguerre RSI,
- 做多条件
- Laguerre 接近 0(
LaguerreEntryTolerance用来模拟原始的== 0判断)。 - EMA 相比上一根完结 K 线向上倾斜。
- CCI 低于
-CciThreshold。
- Laguerre 接近 0(
- 做空条件
- Laguerre 接近 1(
1 - LaguerreEntryTolerance近似== 1判断)。 - EMA 斜率向下。
- CCI 高于
+CciThreshold。
- Laguerre 接近 1(
- 离场规则
- 多单在 Laguerre 升破
LaguerreExitHigh(默认0.9)或价格上涨TakeProfitPoints * PriceStep时平仓。 - 空单在 Laguerre 跌破
LaguerreExitLow(默认0.1)或价格下跌相同距离时平仓。 - 任何外部平仓都会重置内部状态,避免再次使用过期的入场信息。
- 多单在 Laguerre 升破
资金管理
CalculateOrderVolume 函数按照原始 LotsOptimized() 的思路工作:
- 基于风险的手数 —— 使用
equity * MaximumRisk计算风险资本,并除以RiskDivider(默认 500,对应原策略的/500规则)。再除以当前价格得到风险手数。 - 基准手数 —— 如果风险手数低于
BaseVolume,则使用基础手数。 - 连续亏损减仓 —— 当出现两笔及以上连续亏损时,按
volume * losses / DecreaseFactor的公式减少手数,完全对应 MQL4 历史循环。 - 归一化 —— 手数会按照交易品种的
VolumeStep对齐,并限制在MinVolume与MaxVolume之间,避免下单被拒。
盈利后亏损计数清零,亏损则累加,持平保持不变,与原版处理零利润订单的方式一致。
参数
| 名称 | 类型 | 默认值 | 说明 |
|---|---|---|---|
BaseVolume |
decimal |
1.2 |
当风险手数不足时使用的最小下单量。 |
MaximumRisk |
decimal |
0.036 |
建仓时使用的风险资本占比。 |
RiskDivider |
decimal |
500 |
风险资本除数,对应原公式中的 /500。 |
DecreaseFactor |
decimal |
2 |
连续亏损后减少手数所用的因子。 |
MaPeriod |
int |
5 |
作用于中间价的 EMA 周期。 |
CciPeriod |
int |
14 |
CCI 回看长度。 |
CciThreshold |
decimal |
5 |
触发信号所需的 CCI 绝对值。 |
LaguerreGamma |
decimal |
0.66 |
Laguerre 滤波的平滑系数。 |
LaguerreEntryTolerance |
decimal |
0.02 |
判断 Laguerre 是否接近 0/1 的容差。 |
LaguerreExitHigh |
decimal |
0.9 |
多头离场的 Laguerre 上限。 |
LaguerreExitLow |
decimal |
0.1 |
空头离场的 Laguerre 下限。 |
TakeProfitPoints |
decimal |
10 |
以价格点表示的止盈距离(等价于 MQL 中的 Point * Stop)。 |
CandleType |
DataType |
TimeFrame(5m) |
策略订阅的蜡烛类型。 |
实现要点
- Laguerre RSI 在策略内部直接实现为四级递归,无需调用
GetValue()。 - EMA 与 CCI 在蜡烛回调中手动更新,确保输入与 MT4 的
PRICE_MEDIAN完全一致。 - 入场前会检查
AllowLong()/AllowShort()以及是否存在活动订单,保证策略始终只有一张持仓。 - 通过最新成交价、收盘价或开盘价评估盈亏方向,从而维护连续亏损计数。
- 关键逻辑均配有英文注释,便于阅读与二次开发。
使用建议
- 原始策略面向外汇日内行情,建议选择价格步长较小的品种,使默认 10 点目标约等于 1 个点(pip)。
- 为避免部分成交和多笔未完成订单的干扰,可在历史回测或高流动性市场中运行本策略。
- 若 Laguerre 很少触及 0 或 1,可适当提高
LaguerreEntryTolerance。 RiskDivider与DecreaseFactor需要结合调节,以平衡收益扩张与回撤控制。
namespace StockSharp.Samples.Strategies;
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;
/// <summary>
/// Conversion of the MetaTrader 4 expert advisor "Starter" (2005 release).
/// Combines a Laguerre RSI proxy, EMA slope confirmation and a CCI filter.
/// Implements adaptive lot sizing inspired by the original LotsOptimized routine.
/// </summary>
public class Starter2005Strategy : Strategy
{
private readonly StrategyParam<decimal> _baseVolume;
private readonly StrategyParam<decimal> _maximumRisk;
private readonly StrategyParam<decimal> _riskDivider;
private readonly StrategyParam<decimal> _decreaseFactor;
private readonly StrategyParam<int> _maPeriod;
private readonly StrategyParam<int> _cciPeriod;
private readonly StrategyParam<decimal> _cciThreshold;
private readonly StrategyParam<decimal> _laguerreGamma;
private readonly StrategyParam<decimal> _laguerreEntryTolerance;
private readonly StrategyParam<decimal> _laguerreExitHigh;
private readonly StrategyParam<decimal> _laguerreExitLow;
private readonly StrategyParam<decimal> _takeProfitPoints;
private readonly StrategyParam<DataType> _candleType;
private ExponentialMovingAverage _ema = null!;
private CommodityChannelIndex _cci = null!;
private decimal? _previousMa;
private decimal _lagL0;
private decimal _lagL1;
private decimal _lagL2;
private decimal _lagL3;
private bool _laguerreFormed;
private decimal? _entryPrice;
private decimal _entryVolume;
private Sides? _entrySide;
private int _consecutiveLosses;
/// <summary>
/// Initializes a new instance of the <see cref="Starter2005Strategy"/> class.
/// </summary>
public Starter2005Strategy()
{
_baseVolume = Param(nameof(BaseVolume), 1.2m)
.SetGreaterThanZero()
.SetDisplay("Base Volume", "Initial lot size used when risk-based sizing is unavailable", "Risk Management")
.SetOptimize(0.1m, 5m, 0.1m);
_maximumRisk = Param(nameof(MaximumRisk), 0.036m)
.SetNotNegative()
.SetDisplay("Maximum Risk", "Fraction of account equity considered for sizing", "Risk Management")
.SetOptimize(0m, 0.1m, 0.005m);
_riskDivider = Param(nameof(RiskDivider), 500m)
.SetGreaterThanZero()
.SetDisplay("Risk Divider", "Divisor applied to risk capital (mimics the original /500 rule)", "Risk Management")
.SetOptimize(100m, 1000m, 50m);
_decreaseFactor = Param(nameof(DecreaseFactor), 2m)
.SetGreaterThanZero()
.SetDisplay("Decrease Factor", "Lot reduction factor after consecutive losses", "Risk Management")
.SetOptimize(1m, 5m, 0.5m);
_maPeriod = Param(nameof(MaPeriod), 5)
.SetGreaterThanZero()
.SetDisplay("EMA Period", "Length of the exponential moving average applied to median price", "Indicators")
.SetOptimize(3, 30, 1);
_cciPeriod = Param(nameof(CciPeriod), 14)
.SetGreaterThanZero()
.SetDisplay("CCI Period", "Commodity Channel Index lookback length", "Indicators")
.SetOptimize(5, 40, 1);
_cciThreshold = Param(nameof(CciThreshold), 5m)
.SetNotNegative()
.SetDisplay("CCI Threshold", "Absolute CCI level required for signals", "Indicators")
.SetOptimize(1m, 50m, 1m);
_laguerreGamma = Param(nameof(LaguerreGamma), 0.66m)
.SetRange(0.1m, 0.9m)
.SetDisplay("Laguerre Gamma", "Smoothing factor of the Laguerre RSI filter", "Indicators")
.SetOptimize(0.3m, 0.9m, 0.05m);
_laguerreEntryTolerance = Param(nameof(LaguerreEntryTolerance), 0.02m)
.SetRange(0m, 0.3m)
.SetDisplay("Laguerre Entry Tolerance", "Closeness to 0/1 required to mimic the original equality checks", "Signals")
.SetOptimize(0.005m, 0.1m, 0.005m);
_laguerreExitHigh = Param(nameof(LaguerreExitHigh), 0.9m)
.SetRange(0.5m, 1m)
.SetDisplay("Laguerre Exit High", "Upper exit level for long positions", "Signals")
.SetOptimize(0.6m, 1m, 0.05m);
_laguerreExitLow = Param(nameof(LaguerreExitLow), 0.1m)
.SetRange(0m, 0.5m)
.SetDisplay("Laguerre Exit Low", "Lower exit level for short positions", "Signals")
.SetOptimize(0m, 0.4m, 0.05m);
_takeProfitPoints = Param(nameof(TakeProfitPoints), 10m)
.SetNotNegative()
.SetDisplay("Take Profit (points)", "Distance in price points before profit is locked", "Risk Management")
.SetOptimize(0m, 50m, 5m);
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Primary timeframe processed by the strategy", "General");
}
/// <summary>
/// Base lot size used when the risk model produces a smaller value.
/// </summary>
public decimal BaseVolume
{
get => _baseVolume.Value;
set => _baseVolume.Value = value;
}
/// <summary>
/// Fraction of the portfolio considered for risk-based sizing.
/// </summary>
public decimal MaximumRisk
{
get => _maximumRisk.Value;
set => _maximumRisk.Value = value;
}
/// <summary>
/// Divider applied to the risk capital (mirrors the /500 rule).
/// </summary>
public decimal RiskDivider
{
get => _riskDivider.Value;
set => _riskDivider.Value = value;
}
/// <summary>
/// Lot reduction factor after consecutive losses.
/// </summary>
public decimal DecreaseFactor
{
get => _decreaseFactor.Value;
set => _decreaseFactor.Value = value;
}
/// <summary>
/// EMA length applied to median price.
/// </summary>
public int MaPeriod
{
get => _maPeriod.Value;
set => _maPeriod.Value = value;
}
/// <summary>
/// CCI lookback period.
/// </summary>
public int CciPeriod
{
get => _cciPeriod.Value;
set => _cciPeriod.Value = value;
}
/// <summary>
/// Absolute CCI level required for entry.
/// </summary>
public decimal CciThreshold
{
get => _cciThreshold.Value;
set => _cciThreshold.Value = value;
}
/// <summary>
/// Laguerre smoothing factor (gamma).
/// </summary>
public decimal LaguerreGamma
{
get => _laguerreGamma.Value;
set => _laguerreGamma.Value = value;
}
/// <summary>
/// Tolerance applied when checking Laguerre against 0 or 1.
/// </summary>
public decimal LaguerreEntryTolerance
{
get => _laguerreEntryTolerance.Value;
set => _laguerreEntryTolerance.Value = value;
}
/// <summary>
/// Laguerre exit threshold for long positions.
/// </summary>
public decimal LaguerreExitHigh
{
get => _laguerreExitHigh.Value;
set => _laguerreExitHigh.Value = value;
}
/// <summary>
/// Laguerre exit threshold for short positions.
/// </summary>
public decimal LaguerreExitLow
{
get => _laguerreExitLow.Value;
set => _laguerreExitLow.Value = value;
}
/// <summary>
/// Profit distance expressed in price points.
/// </summary>
public decimal TakeProfitPoints
{
get => _takeProfitPoints.Value;
set => _takeProfitPoints.Value = value;
}
/// <summary>
/// Candle type processed by the strategy.
/// </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();
_ema = null!;
_cci = null!;
_previousMa = null;
_lagL0 = _lagL1 = _lagL2 = _lagL3 = 0m;
_laguerreFormed = false;
_entryPrice = null;
_entryVolume = 0m;
_entrySide = null;
_consecutiveLosses = 0;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_ema = new EMA { Length = MaPeriod };
_cci = new CommodityChannelIndex { Length = CciPeriod };
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(_ema, ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, _ema);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle, decimal ma)
{
if (candle.State != CandleStates.Finished)
return;
// Process CCI manually
_cci.Process(candle);
if (!_ema.IsFormed || !_cci.IsFormed)
{
_previousMa = ma;
return;
}
var cci = _cci.GetCurrentValue<decimal>();
var laguerre = CalculateLaguerre(candle.ClosePrice);
if (!_laguerreFormed)
{
_previousMa = ma;
return;
}
var previousMa = _previousMa;
_previousMa = ma;
if (!previousMa.HasValue)
return;
var maRising = ma > previousMa.Value;
var maFalling = ma < previousMa.Value;
var entryTolerance = LaguerreEntryTolerance;
var takeProfitDistance = GetTakeProfitDistance();
var price = GetDecisionPrice(candle);
if (Position == 0m && !HasActiveOrders())
{
if (maRising && laguerre <= entryTolerance && cci < -CciThreshold)
{
var volume = CalculateOrderVolume(price);
if (volume > 0m)
{
BuyMarket(volume);
_entrySide = Sides.Buy;
_entryPrice = price;
_entryVolume = volume;
LogInfo($"Opening long. Laguerre={laguerre:F4}, CCI={cci:F2}, EMA rising.");
}
}
else if (maFalling && laguerre >= 1m - entryTolerance && cci > CciThreshold)
{
var volume = CalculateOrderVolume(price);
if (volume > 0m)
{
SellMarket(volume);
_entrySide = Sides.Sell;
_entryPrice = price;
_entryVolume = volume;
LogInfo($"Opening short. Laguerre={laguerre:F4}, CCI={cci:F2}, EMA falling.");
}
}
}
if (_entrySide == Sides.Buy && Position > 0m && _entryPrice.HasValue)
{
var gain = price - _entryPrice.Value;
if ((LaguerreExitHigh > 0m && laguerre >= LaguerreExitHigh) || (takeProfitDistance > 0m && gain >= takeProfitDistance))
{
var volume = Math.Abs(Position);
if (volume <= 0m)
volume = _entryVolume;
if (volume > 0m && !HasActiveOrders())
{
SellMarket(volume);
RegisterTradeResult(gain);
ResetPositionState();
LogInfo($"Closing long. Laguerre={laguerre:F4}, gain={gain:F5}.");
}
}
}
else if (_entrySide == Sides.Sell && Position < 0m && _entryPrice.HasValue)
{
var gain = _entryPrice.Value - price;
if ((LaguerreExitLow > 0m && laguerre <= LaguerreExitLow) || (takeProfitDistance > 0m && gain >= takeProfitDistance))
{
var volume = Math.Abs(Position);
if (volume <= 0m)
volume = _entryVolume;
if (volume > 0m && !HasActiveOrders())
{
BuyMarket(volume);
RegisterTradeResult(gain);
ResetPositionState();
LogInfo($"Closing short. Laguerre={laguerre:F4}, gain={gain:F5}.");
}
}
}
else if (Position == 0m && !HasActiveOrders())
{
ResetPositionState();
}
}
private decimal CalculateLaguerre(decimal price)
{
var gamma = LaguerreGamma;
var l0Prev = _lagL0;
var l1Prev = _lagL1;
var l2Prev = _lagL2;
var l3Prev = _lagL3;
_lagL0 = (1m - gamma) * price + gamma * l0Prev;
_lagL1 = -gamma * _lagL0 + l0Prev + gamma * l1Prev;
_lagL2 = -gamma * _lagL1 + l1Prev + gamma * l2Prev;
_lagL3 = -gamma * _lagL2 + l2Prev + gamma * l3Prev;
decimal cu = 0m;
decimal cd = 0m;
if (_lagL0 >= _lagL1)
cu = _lagL0 - _lagL1;
else
cd = _lagL1 - _lagL0;
if (_lagL1 >= _lagL2)
cu += _lagL1 - _lagL2;
else
cd += _lagL2 - _lagL1;
if (_lagL2 >= _lagL3)
cu += _lagL2 - _lagL3;
else
cd += _lagL3 - _lagL2;
var denominator = cu + cd;
var result = denominator == 0m ? 0m : cu / denominator;
_laguerreFormed = true;
return result;
}
private decimal CalculateOrderVolume(decimal price)
{
var volume = BaseVolume;
if (MaximumRisk > 0m && RiskDivider > 0m)
{
var portfolio = Portfolio;
var equity = portfolio?.CurrentValue ?? portfolio?.BeginValue ?? 0m;
if (equity > 0m && price > 0m)
{
var riskVolume = equity * MaximumRisk / RiskDivider;
riskVolume /= price;
if (riskVolume > volume)
volume = riskVolume;
}
}
if (DecreaseFactor > 0m && _consecutiveLosses > 1)
{
var reduction = volume * _consecutiveLosses / DecreaseFactor;
volume -= reduction;
}
return NormalizeVolume(volume);
}
private decimal NormalizeVolume(decimal volume)
{
var security = Security;
if (security != null)
{
var step = security.VolumeStep ?? 0m;
if (step <= 0m)
step = 1m;
var minVolume = security.MinVolume ?? step;
var maxVolume = security.MaxVolume;
var steps = decimal.Floor(volume / step);
if (steps < 1m)
steps = 1m;
volume = steps * step;
if (volume < minVolume)
volume = minVolume;
if (maxVolume is decimal max && max > 0m && volume > max)
volume = max;
}
if (volume <= 0m)
volume = 1m;
return volume;
}
private decimal GetTakeProfitDistance()
{
if (TakeProfitPoints <= 0m)
return 0m;
var point = Security?.PriceStep ?? 0m;
if (point <= 0m)
{
var decimals = Security?.Decimals ?? 4;
point = 1m;
for (var i = 0; i < decimals; i++)
point /= 10m;
}
return TakeProfitPoints * point;
}
private decimal GetDecisionPrice(ICandleMessage candle)
{
if (candle.ClosePrice > 0m)
return candle.ClosePrice;
return candle.OpenPrice;
}
private bool HasActiveOrders()
{
foreach (var order in Orders)
{
if (order.State == OrderStates.Active)
return true;
}
return false;
}
private void RegisterTradeResult(decimal gain)
{
if (gain > 0m)
{
if (_consecutiveLosses > 0)
LogInfo($"Profit resets loss streak of {_consecutiveLosses} trades.");
_consecutiveLosses = 0;
}
else if (gain < 0m)
{
_consecutiveLosses++;
LogInfo($"Loss streak increased to {_consecutiveLosses}.");
}
}
private void ResetPositionState()
{
_entryPrice = null;
_entryVolume = 0m;
_entrySide = null;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.BusinessEntities")
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, Sides
from StockSharp.Algo.Strategies import Strategy
from StockSharp.Algo.Indicators import ExponentialMovingAverage, CommodityChannelIndex, CandleIndicatorValue
class starter2005_strategy(Strategy):
def __init__(self):
super(starter2005_strategy, self).__init__()
self._base_volume = self.Param("BaseVolume", 1.2) \
.SetDisplay("Base Volume", "Initial lot size used when risk-based sizing is unavailable", "Risk Management")
self._maximum_risk = self.Param("MaximumRisk", 0.036) \
.SetDisplay("Maximum Risk", "Fraction of account equity considered for sizing", "Risk Management")
self._risk_divider = self.Param("RiskDivider", 500.0) \
.SetDisplay("Risk Divider", "Divisor applied to risk capital", "Risk Management")
self._decrease_factor = self.Param("DecreaseFactor", 2.0) \
.SetDisplay("Decrease Factor", "Lot reduction factor after consecutive losses", "Risk Management")
self._ma_period = self.Param("MaPeriod", 5) \
.SetDisplay("EMA Period", "Length of the exponential moving average", "Indicators")
self._cci_period = self.Param("CciPeriod", 14) \
.SetDisplay("CCI Period", "Commodity Channel Index lookback length", "Indicators")
self._cci_threshold = self.Param("CciThreshold", 5.0) \
.SetDisplay("CCI Threshold", "Absolute CCI level required for signals", "Indicators")
self._laguerre_gamma = self.Param("LaguerreGamma", 0.66) \
.SetDisplay("Laguerre Gamma", "Smoothing factor of the Laguerre RSI filter", "Indicators")
self._laguerre_entry_tolerance = self.Param("LaguerreEntryTolerance", 0.02) \
.SetDisplay("Laguerre Entry Tolerance", "Closeness to 0/1 required for entry", "Signals")
self._laguerre_exit_high = self.Param("LaguerreExitHigh", 0.9) \
.SetDisplay("Laguerre Exit High", "Upper exit level for long positions", "Signals")
self._laguerre_exit_low = self.Param("LaguerreExitLow", 0.1) \
.SetDisplay("Laguerre Exit Low", "Lower exit level for short positions", "Signals")
self._take_profit_points = self.Param("TakeProfitPoints", 10.0) \
.SetDisplay("Take Profit (points)", "Distance in price points before profit is locked", "Risk Management")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))) \
.SetDisplay("Candle Type", "Primary timeframe processed by the strategy", "General")
self._ema = None
self._cci = None
self._previous_ma = None
self._lag_l0 = 0.0
self._lag_l1 = 0.0
self._lag_l2 = 0.0
self._lag_l3 = 0.0
self._laguerre_formed = False
self._entry_price = None
self._entry_volume = 0.0
self._entry_side = None
self._consecutive_losses = 0
@property
def BaseVolume(self):
return self._base_volume.Value
@property
def MaximumRisk(self):
return self._maximum_risk.Value
@property
def RiskDivider(self):
return self._risk_divider.Value
@property
def DecreaseFactor(self):
return self._decrease_factor.Value
@property
def MaPeriod(self):
return self._ma_period.Value
@property
def CciPeriod(self):
return self._cci_period.Value
@property
def CciThreshold(self):
return self._cci_threshold.Value
@property
def LaguerreGamma(self):
return self._laguerre_gamma.Value
@property
def LaguerreEntryTolerance(self):
return self._laguerre_entry_tolerance.Value
@property
def LaguerreExitHigh(self):
return self._laguerre_exit_high.Value
@property
def LaguerreExitLow(self):
return self._laguerre_exit_low.Value
@property
def TakeProfitPoints(self):
return self._take_profit_points.Value
@property
def CandleType(self):
return self._candle_type.Value
def OnStarted2(self, time):
super(starter2005_strategy, self).OnStarted2(time)
self._ema = ExponentialMovingAverage()
self._ema.Length = self.MaPeriod
self._cci = CommodityChannelIndex()
self._cci.Length = self.CciPeriod
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self._ema, self.ProcessCandle).Start()
def ProcessCandle(self, candle, ma_value):
if candle.State != CandleStates.Finished:
return
ma = float(ma_value)
cci_result = self._cci.Process(CandleIndicatorValue(self._cci, candle))
cci = float(cci_result) if cci_result is not None else 0.0
if not self._ema.IsFormed or not self._cci.IsFormed:
self._previous_ma = ma
return
laguerre = self._calculate_laguerre(float(candle.ClosePrice))
if not self._laguerre_formed:
self._previous_ma = ma
return
previous_ma = self._previous_ma
self._previous_ma = ma
if previous_ma is None:
return
ma_rising = ma > previous_ma
ma_falling = ma < previous_ma
entry_tolerance = float(self.LaguerreEntryTolerance)
tp_distance = self._get_take_profit_distance()
price = self._get_decision_price(candle)
if self.Position == 0:
if ma_rising and laguerre <= entry_tolerance and cci < -float(self.CciThreshold):
volume = self._calculate_order_volume(price)
if volume > 0:
self.BuyMarket(volume)
self._entry_side = Sides.Buy
self._entry_price = price
self._entry_volume = volume
elif ma_falling and laguerre >= 1.0 - entry_tolerance and cci > float(self.CciThreshold):
volume = self._calculate_order_volume(price)
if volume > 0:
self.SellMarket(volume)
self._entry_side = Sides.Sell
self._entry_price = price
self._entry_volume = volume
if self._entry_side == Sides.Buy and self.Position > 0 and self._entry_price is not None:
gain = price - self._entry_price
exit_high = float(self.LaguerreExitHigh)
if (exit_high > 0 and laguerre >= exit_high) or (tp_distance > 0 and gain >= tp_distance):
volume = abs(self.Position)
if volume <= 0:
volume = self._entry_volume
if volume > 0:
self.SellMarket(volume)
self._register_trade_result(gain)
self._reset_position_state()
elif self._entry_side == Sides.Sell and self.Position < 0 and self._entry_price is not None:
gain = self._entry_price - price
exit_low = float(self.LaguerreExitLow)
if (exit_low > 0 and laguerre <= exit_low) or (tp_distance > 0 and gain >= tp_distance):
volume = abs(self.Position)
if volume <= 0:
volume = self._entry_volume
if volume > 0:
self.BuyMarket(volume)
self._register_trade_result(gain)
self._reset_position_state()
elif self.Position == 0:
self._reset_position_state()
def _calculate_laguerre(self, price):
gamma = float(self.LaguerreGamma)
l0_prev = self._lag_l0
l1_prev = self._lag_l1
l2_prev = self._lag_l2
l3_prev = self._lag_l3
self._lag_l0 = (1.0 - gamma) * price + gamma * l0_prev
self._lag_l1 = -gamma * self._lag_l0 + l0_prev + gamma * l1_prev
self._lag_l2 = -gamma * self._lag_l1 + l1_prev + gamma * l2_prev
self._lag_l3 = -gamma * self._lag_l2 + l2_prev + gamma * l3_prev
cu = 0.0
cd = 0.0
if self._lag_l0 >= self._lag_l1:
cu = self._lag_l0 - self._lag_l1
else:
cd = self._lag_l1 - self._lag_l0
if self._lag_l1 >= self._lag_l2:
cu += self._lag_l1 - self._lag_l2
else:
cd += self._lag_l2 - self._lag_l1
if self._lag_l2 >= self._lag_l3:
cu += self._lag_l2 - self._lag_l3
else:
cd += self._lag_l3 - self._lag_l2
denominator = cu + cd
result = 0.0 if denominator == 0 else cu / denominator
self._laguerre_formed = True
return result
def _calculate_order_volume(self, price):
volume = float(self.BaseVolume)
max_risk = float(self.MaximumRisk)
risk_divider = float(self.RiskDivider)
if max_risk > 0 and risk_divider > 0:
equity = 0.0
if self.Portfolio is not None:
cv = self.Portfolio.CurrentValue
if cv is not None and float(cv) > 0:
equity = float(cv)
elif self.Portfolio.BeginValue is not None:
equity = float(self.Portfolio.BeginValue)
if equity > 0 and price > 0:
risk_volume = equity * max_risk / risk_divider
risk_volume = risk_volume / price
if risk_volume > volume:
volume = risk_volume
decrease_factor = float(self.DecreaseFactor)
if decrease_factor > 0 and self._consecutive_losses > 1:
reduction = volume * self._consecutive_losses / decrease_factor
volume -= reduction
return self._normalize_volume(volume)
def _normalize_volume(self, volume):
if self.Security is not None:
vs = self.Security.VolumeStep
step = float(vs) if vs is not None and float(vs) > 0 else 1.0
min_vol_sec = self.Security.MinVolume
min_vol = float(min_vol_sec) if min_vol_sec is not None else step
max_vol_sec = self.Security.MaxVolume
import math
steps = math.floor(volume / step)
if steps < 1:
steps = 1
volume = steps * step
if volume < min_vol:
volume = min_vol
if max_vol_sec is not None and float(max_vol_sec) > 0 and volume > float(max_vol_sec):
volume = float(max_vol_sec)
if volume <= 0:
volume = 1.0
return volume
def _get_take_profit_distance(self):
tp_pts = float(self.TakeProfitPoints)
if tp_pts <= 0:
return 0.0
point = 0.0
if self.Security is not None and self.Security.PriceStep is not None:
point = float(self.Security.PriceStep)
if point <= 0:
decimals = 4
if self.Security is not None and self.Security.Decimals is not None:
decimals = int(self.Security.Decimals)
point = 1.0
for _ in range(decimals):
point /= 10.0
return tp_pts * point
def _get_decision_price(self, candle):
cp = float(candle.ClosePrice)
if cp > 0:
return cp
return float(candle.OpenPrice)
def _register_trade_result(self, gain):
if gain > 0:
self._consecutive_losses = 0
elif gain < 0:
self._consecutive_losses += 1
def _reset_position_state(self):
self._entry_price = None
self._entry_volume = 0.0
self._entry_side = None
def OnReseted(self):
super(starter2005_strategy, self).OnReseted()
self._ema = None
self._cci = None
self._previous_ma = None
self._lag_l0 = 0.0
self._lag_l1 = 0.0
self._lag_l2 = 0.0
self._lag_l3 = 0.0
self._laguerre_formed = False
self._entry_price = None
self._entry_volume = 0.0
self._entry_side = None
self._consecutive_losses = 0
def CreateClone(self):
return starter2005_strategy()