Proper Bot 策略
概述
Proper Bot 策略 是从 MetaTrader 4 的 "Proper Bot" 智能交易系统移植而来的网格化交易模型。策略通过初始方向性头寸启动一个订单篮子,根据可配置的距离/手数映射逐步加仓,并结合时间、成交量与价格过滤器管理整个交易周期。该 C# 版本基于 StockSharp 的高级蜡烛订阅与指标接口实现。
工作原理
- 信号判定
- 当启用 EMA 过滤器时,策略在选定的蜡烛序列上计算快、中、慢三条指数移动平均线。快线与慢线的交叉决定方向,中线用于过滤尚未确认的趋势信号。
- 当关闭过滤器时,算法直接使用上一根已完成蜡烛的实体方向。
- 交易前过滤
- 使用成交量的简单移动平均,只有当平均值高于阈值时才允许进场。
- 交易仅在配置的会话起止时间内执行。
- 价格高于
HighLevel时阻止做多,低于LowLevel时阻止做空;极端突破这些边界会强制产生对应方向的信号。
- 网格扩张
- 初始市价单使用
FirstVolume参数。之后的加仓按照GridMap中的 "距离/手数" 对逐级执行,当价格相对当前方向回撤到指定距离时,按映射手数加仓。 - 距离以交易品种的
PriceStep转换为价格差;若未提供步长则使用 0.0001 作为默认值。
- 初始市价单使用
- 风险管理
- 整个订单篮子共享基于加权平均持仓价的整体止盈与止损距离。
- 浮动利润累计到激活阈值后启动跟踪止盈,只要回撤超过
TrailStepPoints就平仓所有持仓。 - 任一退出条件触发时,策略通过市价单平掉整个网格并重置状态。
参数
| 参数 | 说明 | 默认值 |
|---|---|---|
FastMaPeriod |
进场过滤使用的快 EMA 长度。 | 10 |
MidMaPeriod |
可选中间 EMA 长度,必须位于快慢线之间才确认信号,设为 0 可禁用。 | 25 |
SlowMaPeriod |
进场过滤使用的慢 EMA 长度。 | 50 |
DisableMaFilter |
启用后忽略 EMA 条件,直接跟随上一根蜡烛方向。 | true |
VolumePeriod |
成交量平均周期,设为 0 表示不启用。 | 1 |
VolumeMinimum |
允许开仓的最小平均成交量。 | 69 |
HighLevel |
高于此价格禁止做多,并可能触发做空。 | 1.50001 |
LowLevel |
低于此价格禁止做空,并可能触发做多。 | 1.40001 |
FirstVolume |
每次网格循环的第一笔订单手数。 | 0.08 |
GridMap |
用空格分隔的 距离/手数 列表,定义加仓触发距离和对应手数。 |
120/0.1 ... 120/0.19 |
TakeProfitPoints |
应用于整体仓位加权成本的止盈距离(以价格步长计)。 | 10000 |
StopLossPoints |
应用于整体仓位加权成本的止损距离(以价格步长计)。 | 30000 |
TrailStartPoints |
启动跟踪止盈前所需的最小浮盈。 | 52 |
TrailDistancePoints |
激活跟踪止盈所需的利润距离(减去跟踪步长)。 | 52 |
TrailStepPoints |
跟踪止盈允许的最大利润回吐。 | 2 |
StartHour / StartMinute |
交易会话起始时间(含)。 | 06:00 |
FinishHour / FinishMinute |
交易会话结束时间(含,可跨夜)。 | 21:00 |
CandleType |
策略处理的蜡烛数据类型。 | 1 分钟时间框架 |
使用说明
GridMap采用不变文化格式解析,请在斜杠前输入点数距离,在斜杠后输入手数。- 所有止盈、止损与跟踪参数均会依据品种的
PriceStep转换为实际价格差。运行前请确认证券对象的步长设置正确。 - 跟踪止盈按照所有持仓的浮动利润之和工作,并在蜡烛收盘时检查;如需更快退出可使用更小的时间框架。
HighLevel与LowLevel触发的强制进场使用蜡烛收盘价近似买卖价差。- 与 MT4 版本不同,本实现检测到止盈、止损或跟踪条件时会一次性平掉全部仓位,便于使用高级 API 管理订单。
与 MT4 版本的差异
- MT4 版本为每个订单单独设置保护价位,StockSharp 版本则针对组合头寸计算统一的退出点。
- 由于蜡烛订阅默认不包含逐笔价差,买卖价使用蜡烛收盘价近似。
- 跟踪退出的激活阈值取
TrailDistancePoints - TrailStepPoints与TrailStartPoints中较大者,以保证参数重叠时的稳定性。 - 交易时段依据蜡烛的
DateTimeOffset,请确保数据源时间符合预期时区。
文件
CS/ProperBotStrategy.cs– 策略实现。README.md– 英文说明。README_zh.md– 中文说明(本文)。README_ru.md– 俄文说明。
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;
using System.Globalization;
using StockSharp.Algo;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Grid strategy converted from the "Proper Bot" MQL expert advisor.
/// </summary>
public class ProperBotStrategy : Strategy
{
private readonly StrategyParam<int> _fastPeriod;
private readonly StrategyParam<int> _midPeriod;
private readonly StrategyParam<int> _slowPeriod;
private readonly StrategyParam<bool> _disableMaFilter;
private readonly StrategyParam<int> _volumePeriod;
private readonly StrategyParam<decimal> _volumeMinimum;
private readonly StrategyParam<decimal> _highLevel;
private readonly StrategyParam<decimal> _lowLevel;
private readonly StrategyParam<decimal> _firstVolume;
private readonly StrategyParam<string> _gridMap;
private readonly StrategyParam<int> _takeProfitPoints;
private readonly StrategyParam<int> _stopLossPoints;
private readonly StrategyParam<int> _trailStartPoints;
private readonly StrategyParam<int> _trailDistancePoints;
private readonly StrategyParam<int> _trailStepPoints;
private readonly StrategyParam<int> _startHour;
private readonly StrategyParam<int> _startMinute;
private readonly StrategyParam<int> _finishHour;
private readonly StrategyParam<int> _finishMinute;
private readonly StrategyParam<DataType> _candleType;
private readonly List<GridLevel> _gridLevels = new();
private readonly List<GridOrder> _activeOrders = new();
private ExponentialMovingAverage _fastEma;
private ExponentialMovingAverage _midEma;
private ExponentialMovingAverage _slowEma;
private SimpleMovingAverage _volumeAverage;
private decimal _priceStep;
private Sides? _currentDirection;
private int _nextGridIndex;
private decimal _lastEntryPrice;
private decimal _maxTrailingPoints;
private bool _hasPreviousCandle;
private decimal _previousOpen;
private decimal _previousClose;
private int _previousSignal;
public int FastMaPeriod
{
get => _fastPeriod.Value;
set => _fastPeriod.Value = value;
}
public int MidMaPeriod
{
get => _midPeriod.Value;
set => _midPeriod.Value = value;
}
public int SlowMaPeriod
{
get => _slowPeriod.Value;
set => _slowPeriod.Value = value;
}
public bool DisableMaFilter
{
get => _disableMaFilter.Value;
set => _disableMaFilter.Value = value;
}
public int VolumePeriod
{
get => _volumePeriod.Value;
set => _volumePeriod.Value = value;
}
public decimal VolumeMinimum
{
get => _volumeMinimum.Value;
set => _volumeMinimum.Value = value;
}
public decimal HighLevel
{
get => _highLevel.Value;
set => _highLevel.Value = value;
}
public decimal LowLevel
{
get => _lowLevel.Value;
set => _lowLevel.Value = value;
}
public decimal FirstVolume
{
get => _firstVolume.Value;
set => _firstVolume.Value = value;
}
public string GridMap
{
get => _gridMap.Value;
set => _gridMap.Value = value;
}
public int TakeProfitPoints
{
get => _takeProfitPoints.Value;
set => _takeProfitPoints.Value = value;
}
public int StopLossPoints
{
get => _stopLossPoints.Value;
set => _stopLossPoints.Value = value;
}
public int TrailStartPoints
{
get => _trailStartPoints.Value;
set => _trailStartPoints.Value = value;
}
public int TrailDistancePoints
{
get => _trailDistancePoints.Value;
set => _trailDistancePoints.Value = value;
}
public int TrailStepPoints
{
get => _trailStepPoints.Value;
set => _trailStepPoints.Value = value;
}
public int StartHour
{
get => _startHour.Value;
set => _startHour.Value = value;
}
public int StartMinute
{
get => _startMinute.Value;
set => _startMinute.Value = value;
}
public int FinishHour
{
get => _finishHour.Value;
set => _finishHour.Value = value;
}
public int FinishMinute
{
get => _finishMinute.Value;
set => _finishMinute.Value = value;
}
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
public ProperBotStrategy()
{
_fastPeriod = Param(nameof(FastMaPeriod), 10)
.SetGreaterThanZero()
.SetDisplay("Fast EMA", "Period of the fast EMA filter", "Signals")
;
_midPeriod = Param(nameof(MidMaPeriod), 25)
.SetDisplay("Mid EMA", "Optional middle EMA period", "Signals")
;
_slowPeriod = Param(nameof(SlowMaPeriod), 50)
.SetGreaterThanZero()
.SetDisplay("Slow EMA", "Period of the slow EMA filter", "Signals")
;
_disableMaFilter = Param(nameof(DisableMaFilter), true)
.SetDisplay("Disable EMA Filter", "Use previous candle direction instead of EMAs", "Signals")
;
_volumePeriod = Param(nameof(VolumePeriod), 1)
.SetDisplay("Volume Period", "Number of candles for the volume filter", "Filters")
;
_volumeMinimum = Param(nameof(VolumeMinimum), 0m)
.SetDisplay("Volume Minimum", "Minimal average volume to allow entries", "Filters")
;
_highLevel = Param(nameof(HighLevel), 1000000m)
.SetDisplay("High Level", "Do not buy above this price", "Filters")
;
_lowLevel = Param(nameof(LowLevel), -1000000m)
.SetDisplay("Low Level", "Do not sell below this price", "Filters")
;
_firstVolume = Param(nameof(FirstVolume), 0.08m)
.SetDisplay("First Order Volume", "Volume for the first order in a grid cycle", "Risk")
;
_gridMap = Param(nameof(GridMap), string.Empty)
.SetDisplay("Grid Map", "Distance/volume pairs separated by spaces", "Risk");
_takeProfitPoints = Param(nameof(TakeProfitPoints), 10000)
.SetDisplay("Take Profit Points", "Profit distance in price steps", "Risk")
;
_stopLossPoints = Param(nameof(StopLossPoints), 30000)
.SetDisplay("Stop Loss Points", "Loss distance in price steps", "Risk")
;
_trailStartPoints = Param(nameof(TrailStartPoints), 52)
.SetDisplay("Trail Start Points", "Minimal profit to arm the trailing exit", "Risk")
;
_trailDistancePoints = Param(nameof(TrailDistancePoints), 52)
.SetDisplay("Trail Distance Points", "Profit distance required to enable trailing", "Risk")
;
_trailStepPoints = Param(nameof(TrailStepPoints), 2)
.SetDisplay("Trail Step Points", "Allowed profit retracement before exit", "Risk")
;
_startHour = Param(nameof(StartHour), 0)
.SetDisplay("Start Hour", "Trading session start hour", "Session")
;
_startMinute = Param(nameof(StartMinute), 0)
.SetDisplay("Start Minute", "Trading session start minute", "Session")
;
_finishHour = Param(nameof(FinishHour), 23)
.SetDisplay("Finish Hour", "Trading session end hour", "Session")
;
_finishMinute = Param(nameof(FinishMinute), 0)
.SetDisplay("Finish Minute", "Trading session end minute", "Session")
;
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Type of candles to process", "General");
}
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
=> [(Security, CandleType)];
protected override void OnReseted()
{
base.OnReseted();
_gridLevels.Clear();
_activeOrders.Clear();
_fastEma = null;
_midEma = null;
_slowEma = null;
_volumeAverage = null;
_priceStep = 0m;
_currentDirection = null;
_nextGridIndex = 0;
_lastEntryPrice = 0m;
_maxTrailingPoints = decimal.MinValue;
Volume = FirstVolume;
_hasPreviousCandle = false;
_previousOpen = 0m;
_previousClose = 0m;
_previousSignal = 0;
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
ParseGridMap();
_fastEma = new ExponentialMovingAverage { Length = Math.Max(1, FastMaPeriod) };
_midEma = new ExponentialMovingAverage { Length = Math.Max(1, MidMaPeriod) };
_slowEma = new ExponentialMovingAverage { Length = Math.Max(1, SlowMaPeriod) };
_volumeAverage = new SimpleMovingAverage { Length = Math.Max(1, VolumePeriod) };
_priceStep = Security?.PriceStep ?? 0.0001m;
if (_priceStep <= 0m)
_priceStep = 0.0001m;
_maxTrailingPoints = decimal.MinValue;
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(_fastEma, _midEma, _slowEma, ProcessCandle)
.Start();
StartProtection(
takeProfit: new Unit(2, UnitTypes.Percent),
stopLoss: new Unit(1, UnitTypes.Percent));
}
protected override void OnOwnTradeReceived(MyTrade trade)
{
base.OnOwnTradeReceived(trade);
if (trade == null)
return;
var direction = trade.Order.Side;
var volume = trade.Trade.Volume;
if (volume <= 0m)
return;
if (_currentDirection is null)
_currentDirection = direction;
if (_currentDirection == direction)
{
_activeOrders.Add(new GridOrder(trade.Trade.Price, volume));
_lastEntryPrice = trade.Trade.Price;
if (_activeOrders.Count <= 1)
{
_nextGridIndex = 0;
_maxTrailingPoints = decimal.MinValue;
}
else
{
_nextGridIndex = Math.Min(_activeOrders.Count - 1, Math.Max(0, _gridLevels.Count - 1));
}
}
else
{
ReducePosition(volume);
if (_activeOrders.Count == 0)
{
_currentDirection = null;
_lastEntryPrice = 0m;
_nextGridIndex = 0;
_maxTrailingPoints = decimal.MinValue;
}
else
{
_lastEntryPrice = _activeOrders[^1].Price;
_nextGridIndex = Math.Min(_activeOrders.Count - 1, Math.Max(0, _gridLevels.Count - 1));
}
}
}
private void ProcessCandle(ICandleMessage candle, decimal fastValue, decimal midValue, decimal slowValue)
{
if (candle.State != CandleStates.Finished)
return;
var signal = CalculateSignal(fastValue, midValue, slowValue);
if (Position == 0 && signal != 0 && signal != _previousSignal)
{
if (signal > 0)
BuyMarket();
else
SellMarket();
}
UpdatePreviousCandle(candle);
_previousSignal = signal;
}
private int CalculateSignal(decimal fastValue, decimal midValue, decimal slowValue)
{
if (DisableMaFilter)
{
if (!_hasPreviousCandle)
return 0;
if (_previousClose > _previousOpen)
return 1;
if (_previousClose < _previousOpen)
return -1;
return 0;
}
if (!_slowEma.IsFormed)
return 0;
var fastSignal = fastValue.CompareTo(slowValue);
if (fastSignal == 0)
return 0;
var signal = fastSignal > 0 ? 1 : -1;
if (MidMaPeriod > 0)
{
if (!_midEma.IsFormed)
return 0;
if ((midValue >= fastValue && fastValue > slowValue) || (midValue <= fastValue && fastValue < slowValue))
return 0;
}
return signal;
}
private bool CheckVolume(ICandleMessage candle)
{
if (VolumePeriod < 1)
return true;
var average = _volumeAverage.Process(new DecimalIndicatorValue(_volumeAverage, candle.TotalVolume, candle.CloseTime) { IsFinal = true }).ToDecimal();
if (!_volumeAverage.IsFormed)
return false;
return average >= VolumeMinimum;
}
private void ApplyBoundaryFilters(ref int signal, decimal price)
{
var ask = price;
var bid = price;
if (ask > HighLevel)
signal = 0;
if (bid < LowLevel)
signal = 0;
if (ask < LowLevel)
signal = 1;
if (bid > HighLevel)
signal = -1;
}
private bool IsWithinTradingHours(DateTimeOffset time)
{
var start = new TimeSpan(StartHour, StartMinute, 0);
var end = new TimeSpan(FinishHour, FinishMinute, 0);
var current = time.TimeOfDay;
if (start == end)
return true;
if (start < end)
return current >= start && current <= end;
return current >= start || current <= end;
}
private void StartNewCycle(int signal)
{
if (FirstVolume <= 0m)
return;
if (signal > 0)
{
BuyMarket();
}
else if (signal < 0)
{
SellMarket();
}
}
private bool ManageRisk(ICandleMessage candle)
{
if (_currentDirection is null || _activeOrders.Count == 0)
return false;
var averagePrice = CalculateAveragePrice();
var close = candle.ClosePrice;
var high = candle.HighPrice;
var low = candle.LowPrice;
var direction = _currentDirection == Sides.Buy ? 1m : -1m;
var takeProfitDistance = ConvertPointsToPrice(TakeProfitPoints);
var stopLossDistance = ConvertPointsToPrice(StopLossPoints);
if (direction > 0)
{
if (TakeProfitPoints > 0 && high - averagePrice >= takeProfitDistance)
{
if (Position > 0) SellMarket(); else if (Position < 0) BuyMarket();
return true;
}
if (StopLossPoints > 0 && averagePrice - low >= stopLossDistance)
{
if (Position > 0) SellMarket(); else if (Position < 0) BuyMarket();
return true;
}
}
else
{
if (TakeProfitPoints > 0 && averagePrice - low >= takeProfitDistance)
{
if (Position > 0) SellMarket(); else if (Position < 0) BuyMarket();
return true;
}
if (StopLossPoints > 0 && high - averagePrice >= stopLossDistance)
{
if (Position > 0) SellMarket(); else if (Position < 0) BuyMarket();
return true;
}
}
if (TrailDistancePoints > 0 && TrailStepPoints > 0)
{
var points = CalculateFloatingPoints(close);
var activation = (decimal)Math.Max(TrailDistancePoints - TrailStepPoints, TrailStartPoints);
if (points >= activation)
{
if (_maxTrailingPoints == decimal.MinValue || points > _maxTrailingPoints)
{
_maxTrailingPoints = points;
}
else if (_maxTrailingPoints - points >= TrailStepPoints)
{
if (Position > 0) SellMarket(); else if (Position < 0) BuyMarket();
return true;
}
}
}
return false;
}
private void ProcessGridExpansion(ICandleMessage candle)
{
if (_currentDirection is null || _activeOrders.Count == 0 || _gridLevels.Count == 0)
return;
var index = Math.Min(_nextGridIndex, _gridLevels.Count - 1);
var level = _gridLevels[index];
var distance = level.Distance * _priceStep;
if (distance <= 0m)
return;
if (_currentDirection == Sides.Buy)
{
if (_lastEntryPrice - candle.LowPrice >= distance)
{
if (level.Volume > 0m)
BuyMarket();
}
}
else
{
if (candle.HighPrice - _lastEntryPrice >= distance)
{
if (level.Volume > 0m)
SellMarket();
}
}
}
private void UpdatePreviousCandle(ICandleMessage candle)
{
_previousOpen = candle.OpenPrice;
_previousClose = candle.ClosePrice;
_hasPreviousCandle = true;
}
private decimal CalculateAveragePrice()
{
if (_activeOrders.Count == 0)
return 0m;
decimal sum = 0m;
decimal volume = 0m;
for (var i = 0; i < _activeOrders.Count; i++)
{
var order = _activeOrders[i];
sum += order.Price * order.Volume;
volume += order.Volume;
}
return volume > 0m ? sum / volume : 0m;
}
private decimal CalculateFloatingPoints(decimal price)
{
if (_activeOrders.Count == 0 || _priceStep <= 0m || _currentDirection is null)
return 0m;
var direction = _currentDirection == Sides.Buy ? 1m : -1m;
decimal sum = 0m;
for (var i = 0; i < _activeOrders.Count; i++)
{
var order = _activeOrders[i];
sum += (price - order.Price) * direction / _priceStep;
}
return sum;
}
private decimal ConvertPointsToPrice(int points)
=> points <= 0 ? 0m : points * _priceStep;
private bool HasActiveCycle()
=> _activeOrders.Count > 0;
private void ReducePosition(decimal volume)
{
var remaining = volume;
for (var i = _activeOrders.Count - 1; i >= 0 && remaining > 0m; i--)
{
var order = _activeOrders[i];
if (order.Volume > remaining)
{
order.Volume -= remaining;
remaining = 0m;
}
else
{
remaining -= order.Volume;
_activeOrders.RemoveAt(i);
}
}
}
private void ParseGridMap()
{
_gridLevels.Clear();
if (GridMap.IsEmptyOrWhiteSpace())
return;
var parts = GridMap.Split(new[] { ' ', ';', '\t', '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries);
foreach (var part in parts)
{
var tokens = part.Split('/');
if (tokens.Length != 2)
continue;
if (!decimal.TryParse(tokens[0], NumberStyles.Any, CultureInfo.InvariantCulture, out var distance))
continue;
if (!decimal.TryParse(tokens[1], NumberStyles.Any, CultureInfo.InvariantCulture, out var volume))
continue;
if (distance <= 0m || volume <= 0m)
continue;
_gridLevels.Add(new GridLevel(distance, volume));
}
}
private sealed class GridOrder
{
public GridOrder(decimal price, decimal volume)
{
Price = price;
Volume = volume;
}
public decimal Price { get; set; }
public decimal Volume { get; set; }
}
private readonly record struct GridLevel(decimal Distance, decimal Volume);
}
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, Unit, UnitTypes
from StockSharp.Algo.Indicators import ExponentialMovingAverage
from StockSharp.Algo.Strategies import Strategy
class proper_bot_strategy(Strategy):
def __init__(self):
super(proper_bot_strategy, self).__init__()
self._fast_period = self.Param("FastMaPeriod", 10)
self._mid_period = self.Param("MidMaPeriod", 25)
self._slow_period = self.Param("SlowMaPeriod", 50)
self._disable_ma_filter = self.Param("DisableMaFilter", True)
self._first_volume = self.Param("FirstVolume", 0.08)
self._take_profit_points = self.Param("TakeProfitPoints", 10000)
self._stop_loss_points = self.Param("StopLossPoints", 30000)
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5)))
self._price_step = 1.0
self._has_previous_candle = False
self._previous_open = 0.0
self._previous_close = 0.0
self._previous_signal = 0
self._entry_price = 0.0
self._direction = 0
@property
def FastMaPeriod(self):
return self._fast_period.Value
@FastMaPeriod.setter
def FastMaPeriod(self, value):
self._fast_period.Value = value
@property
def MidMaPeriod(self):
return self._mid_period.Value
@MidMaPeriod.setter
def MidMaPeriod(self, value):
self._mid_period.Value = value
@property
def SlowMaPeriod(self):
return self._slow_period.Value
@SlowMaPeriod.setter
def SlowMaPeriod(self, value):
self._slow_period.Value = value
@property
def DisableMaFilter(self):
return self._disable_ma_filter.Value
@DisableMaFilter.setter
def DisableMaFilter(self, value):
self._disable_ma_filter.Value = value
@property
def FirstVolume(self):
return self._first_volume.Value
@FirstVolume.setter
def FirstVolume(self, value):
self._first_volume.Value = value
@property
def TakeProfitPoints(self):
return self._take_profit_points.Value
@TakeProfitPoints.setter
def TakeProfitPoints(self, value):
self._take_profit_points.Value = value
@property
def StopLossPoints(self):
return self._stop_loss_points.Value
@StopLossPoints.setter
def StopLossPoints(self, value):
self._stop_loss_points.Value = value
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
def OnStarted2(self, time):
super(proper_bot_strategy, self).OnStarted2(time)
self._has_previous_candle = False
self._previous_open = 0.0
self._previous_close = 0.0
self._previous_signal = 0
self._entry_price = 0.0
self._direction = 0
self._price_step = float(self.Security.PriceStep) if self.Security is not None and self.Security.PriceStep is not None else 1.0
if self._price_step <= 0.0:
self._price_step = 1.0
fast_ema = ExponentialMovingAverage()
fast_ema.Length = max(1, int(self.FastMaPeriod))
mid_ema = ExponentialMovingAverage()
mid_ema.Length = max(1, int(self.MidMaPeriod))
slow_ema = ExponentialMovingAverage()
slow_ema.Length = max(1, int(self.SlowMaPeriod))
self._fast_ema = fast_ema
self._mid_ema = mid_ema
self._slow_ema = slow_ema
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(fast_ema, mid_ema, slow_ema, self.ProcessCandle).Start()
self.StartProtection(
Unit(2000.0, UnitTypes.Absolute),
Unit(1000.0, UnitTypes.Absolute))
def ProcessCandle(self, candle, fast_value, mid_value, slow_value):
if candle.State != CandleStates.Finished:
return
fast_val = float(fast_value)
mid_val = float(mid_value)
slow_val = float(slow_value)
close = float(candle.ClosePrice)
high = float(candle.HighPrice)
low = float(candle.LowPrice)
open_price = float(candle.OpenPrice)
if self.Position != 0:
if self._manage_risk(candle):
self._update_previous_candle(candle)
return
signal = self._calculate_signal(fast_val, mid_val, slow_val)
if self.Position == 0 and signal != 0 and signal != self._previous_signal:
if signal > 0:
self.BuyMarket()
self._entry_price = close
self._direction = 1
else:
self.SellMarket()
self._entry_price = close
self._direction = -1
self._update_previous_candle(candle)
self._previous_signal = signal
def _calculate_signal(self, fast_val, mid_val, slow_val):
if self.DisableMaFilter:
if not self._has_previous_candle:
return 0
if self._previous_close > self._previous_open:
return 1
if self._previous_close < self._previous_open:
return -1
return 0
if not self._slow_ema.IsFormed:
return 0
if fast_val > slow_val:
signal = 1
elif fast_val < slow_val:
signal = -1
else:
return 0
mid_period = int(self.MidMaPeriod)
if mid_period > 0 and self._mid_ema.IsFormed:
if (mid_val >= fast_val and fast_val > slow_val) or (mid_val <= fast_val and fast_val < slow_val):
return 0
return signal
def _manage_risk(self, candle):
if self._entry_price <= 0.0:
return False
close = float(candle.ClosePrice)
high = float(candle.HighPrice)
low = float(candle.LowPrice)
tp_dist = int(self.TakeProfitPoints) * self._price_step
sl_dist = int(self.StopLossPoints) * self._price_step
if self._direction > 0:
if tp_dist > 0.0 and high - self._entry_price >= tp_dist:
self.SellMarket()
self._entry_price = 0.0
self._direction = 0
return True
if sl_dist > 0.0 and self._entry_price - low >= sl_dist:
self.SellMarket()
self._entry_price = 0.0
self._direction = 0
return True
elif self._direction < 0:
if tp_dist > 0.0 and self._entry_price - low >= tp_dist:
self.BuyMarket()
self._entry_price = 0.0
self._direction = 0
return True
if sl_dist > 0.0 and high - self._entry_price >= sl_dist:
self.BuyMarket()
self._entry_price = 0.0
self._direction = 0
return True
return False
def _update_previous_candle(self, candle):
self._previous_open = float(candle.OpenPrice)
self._previous_close = float(candle.ClosePrice)
self._has_previous_candle = True
def OnReseted(self):
super(proper_bot_strategy, self).OnReseted()
self._price_step = 1.0
self._has_previous_candle = False
self._previous_open = 0.0
self._previous_close = 0.0
self._previous_signal = 0
self._entry_price = 0.0
self._direction = 0
def CreateClone(self):
return proper_bot_strategy()