Стратегия Cyberia Trader AI
Стратегия представляет собой порт на StockSharp исходного советника CyberiaTrader.mq4 (build 8553). В оригинале алгоритм сочетает вероятностную модель и набор дополнительных фильтров. В переводе сохранена та же логика: модель подбирает лучший период выборки, а затем фильтры MACD, EMA и детектор разворотов могут запретить или разрешить сделки.
Индикаторы и внутренняя модель
- Вероятностный движок — перебирает кандидаты от 1 до
MaxPeriodи для каждого анализируетSamplesPerPeriodисторических отрезков. Для каждого периода вычисляются:- Направление (покупка/продажа/ожидание) на основе последовательностей минутных свечей с шагом равным периоду.
- Средние амплитуды "возможностей" для покупок, продаж и неопределённых состояний, а также доля успешных случаев выше
SpreadThreshold. - Лучший период определяется максимальным отношением успешных исходов.
- EMA-фильтр тренда (
EnableMa) — запрещает сделки против текущего наклона EMA. - MACD-фильтр (
EnableMacd) — блокирует сделки против направленного импульса. - Детектор разворотов (
EnableReversalDetector) — инвертирует разрешения, если текущие возможности превышают средние значения более чем вReversalFactorраз.
Параметры
| Имя | Описание |
|---|---|
MaxPeriod |
Максимальный тестируемый шаг выборки для вероятностной модели. |
SamplesPerPeriod |
Количество отрезков истории на каждый кандидат периода. |
SpreadThreshold |
Минимальная амплитуда, считающаяся успешным исходом. |
EnableCyberiaLogic |
Включает оригинальные переключатели, блокирующие покупки или продажи. |
EnableMacd |
Включает фильтр MACD. |
EnableMa |
Включает фильтр по наклону EMA. |
EnableReversalDetector |
Включает детектор резких разворотов. |
MaPeriod |
Длина EMA в трендовом фильтре. |
MacdFast / MacdSlow / MacdSignal |
Периоды MACD (быстрый, медленный и сигнальный). |
ReversalFactor |
Множитель, запускающий детектор разворотов. |
CandleType |
Тип используемых свечей (по умолчанию 1 минута). |
TakeProfitPercent |
Процентное значение take-profit. |
StopLossPercent |
Процентное значение stop-loss. |
Логика торговли
- После формирования свечи история обновляется, и для всех периодов от 1 до
MaxPeriodпересчитываются статистики. Наилучший период — тот, у которого максимальная доля успешных исходов. - Cyberia-логика выставляет флаги
DisableBuyиDisableSellаналогично MQL:- Сравнивает средние возможности покупок/продаж и их успех при увеличении или уменьшении периода.
- Запрещает новый сигнал, если текущая возможность больше успешной средней более чем вдвое.
- Фильтры применяются последовательно: MACD → EMA → детектор разворотов.
- При отсутствии позиции стратегия открывает сделку по направлению решения, если соответствующая возможность превышает свою успешную среднюю и противоположное направление заблокировано.
- При наличии позиции она закрывается, когда вероятностная модель меняет решение или фильтры запрещают текущее направление.
- Вызов
StartProtectionактивирует защиту позиции при ненулевых параметрах риска.
Особенности перевода
- Вместо проверки по фактическому спреду используется параметр
SpreadThreshold. - Автоматический расчёт объёма и вывод информации о счёте опущены; объём контролируется через свойство
Volumeстратегии. - Блоки MoneyTrain и Pipsator сведены к единой логике входа/выхода в соответствии с высокоуровневым API StockSharp.
- Для наглядности добавлено отображение свечей, EMA и MACD на графике в дизайнере.
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 StockSharp.Algo;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// StockSharp port of the CyberiaTrader (build 8553) expert advisor.
/// Recreates the probability driven decision engine together with optional MA, MACD, and reversal filters.
/// </summary>
public class CyberiaTraderAiStrategy : Strategy
{
private readonly StrategyParam<int> _maxPeriod;
private readonly StrategyParam<int> _samplesPerPeriod;
private readonly StrategyParam<decimal> _spreadThreshold;
private readonly StrategyParam<bool> _enableCyberiaLogic;
private readonly StrategyParam<bool> _enableMacd;
private readonly StrategyParam<bool> _enableMa;
private readonly StrategyParam<bool> _enableReversalDetector;
private readonly StrategyParam<int> _maPeriod;
private readonly StrategyParam<int> _macdFast;
private readonly StrategyParam<int> _macdSlow;
private readonly StrategyParam<int> _macdSignal;
private readonly StrategyParam<decimal> _reversalFactor;
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<decimal> _takeProfitPercent;
private readonly StrategyParam<decimal> _stopLossPercent;
private MovingAverageConvergenceDivergenceSignal _macd;
private ExponentialMovingAverage _ema;
private readonly Queue<CandleSnapshot> _history = new();
private decimal? _previousEma;
private int? _previousPeriod;
private ModelStats _currentStats;
/// <summary>
/// Maximum sampling period evaluated by the probability model.
/// </summary>
public int MaxPeriod
{
get => _maxPeriod.Value;
set => _maxPeriod.Value = value;
}
/// <summary>
/// Number of segments (per period) used for statistical evaluation.
/// </summary>
public int SamplesPerPeriod
{
get => _samplesPerPeriod.Value;
set => _samplesPerPeriod.Value = value;
}
/// <summary>
/// Minimal absolute move that qualifies as a successful probability outcome.
/// </summary>
public decimal SpreadThreshold
{
get => _spreadThreshold.Value;
set => _spreadThreshold.Value = value;
}
/// <summary>
/// Enables the Cyberia probability filter.
/// </summary>
public bool EnableCyberiaLogic
{
get => _enableCyberiaLogic.Value;
set => _enableCyberiaLogic.Value = value;
}
/// <summary>
/// Enables the MACD trend filter.
/// </summary>
public bool EnableMacd
{
get => _enableMacd.Value;
set => _enableMacd.Value = value;
}
/// <summary>
/// Enables the EMA slope filter.
/// </summary>
public bool EnableMa
{
get => _enableMa.Value;
set => _enableMa.Value = value;
}
/// <summary>
/// Enables the reversal detector that flips permissions when extreme spikes appear.
/// </summary>
public bool EnableReversalDetector
{
get => _enableReversalDetector.Value;
set => _enableReversalDetector.Value = value;
}
/// <summary>
/// Length of the EMA trend filter.
/// </summary>
public int MaPeriod
{
get => _maPeriod.Value;
set => _maPeriod.Value = value;
}
/// <summary>
/// Fast period of the MACD module.
/// </summary>
public int MacdFast
{
get => _macdFast.Value;
set => _macdFast.Value = value;
}
/// <summary>
/// Slow period of the MACD module.
/// </summary>
public int MacdSlow
{
get => _macdSlow.Value;
set => _macdSlow.Value = value;
}
/// <summary>
/// Signal period of the MACD module.
/// </summary>
public int MacdSignal
{
get => _macdSignal.Value;
set => _macdSignal.Value = value;
}
/// <summary>
/// Multiplier used by the reversal detector.
/// </summary>
public decimal ReversalFactor
{
get => _reversalFactor.Value;
set => _reversalFactor.Value = value;
}
/// <summary>
/// Candle type processed by the strategy.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Optional take profit distance in percent.
/// </summary>
public decimal TakeProfitPercent
{
get => _takeProfitPercent.Value;
set => _takeProfitPercent.Value = value;
}
/// <summary>
/// Optional stop loss distance in percent.
/// </summary>
public decimal StopLossPercent
{
get => _stopLossPercent.Value;
set => _stopLossPercent.Value = value;
}
/// <summary>
/// Initializes a new instance of <see cref="CyberiaTraderAiStrategy"/>.
/// </summary>
public CyberiaTraderAiStrategy()
{
_maxPeriod = Param(nameof(MaxPeriod), 23)
.SetGreaterThanZero()
.SetDisplay("Max Period", "Largest sampling stride tested by the probability engine", "Model");
_samplesPerPeriod = Param(nameof(SamplesPerPeriod), 5)
.SetGreaterThanZero()
.SetDisplay("Segments Per Period", "Number of historical segments processed for every period candidate", "Model");
_spreadThreshold = Param(nameof(SpreadThreshold), 0m)
.SetNotNegative()
.SetDisplay("Spread Threshold", "Minimal absolute move to count a probability as successful", "Model");
_enableCyberiaLogic = Param(nameof(EnableCyberiaLogic), true)
.SetDisplay("Enable Cyberia Logic", "Use the probability based disable/allow switches", "Filters");
_enableMacd = Param(nameof(EnableMacd), false)
.SetDisplay("Enable MACD", "Use MACD to block trading against momentum", "Filters");
_enableMa = Param(nameof(EnableMa), false)
.SetDisplay("Enable EMA", "Use EMA slope to forbid trades against the trend", "Filters");
_enableReversalDetector = Param(nameof(EnableReversalDetector), false)
.SetDisplay("Enable Reversal Detector", "Flip permissions on extreme probability spikes", "Filters");
_maPeriod = Param(nameof(MaPeriod), 23)
.SetGreaterThanZero()
.SetDisplay("EMA Period", "Length of the EMA used in the trend filter", "Indicators");
_macdFast = Param(nameof(MacdFast), 12)
.SetGreaterThanZero()
.SetDisplay("MACD Fast", "Fast EMA length for MACD", "Indicators");
_macdSlow = Param(nameof(MacdSlow), 26)
.SetGreaterThanZero()
.SetDisplay("MACD Slow", "Slow EMA length for MACD", "Indicators");
_macdSignal = Param(nameof(MacdSignal), 9)
.SetGreaterThanZero()
.SetDisplay("MACD Signal", "Signal EMA length for MACD", "Indicators");
_reversalFactor = Param(nameof(ReversalFactor), 3m)
.SetGreaterThanZero()
.SetDisplay("Reversal Factor", "Threshold multiplier that triggers the reversal detector", "Filters");
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(2).TimeFrame())
.SetDisplay("Candle Type", "Primary timeframe processed by the model", "General");
_takeProfitPercent = Param(nameof(TakeProfitPercent), 0m)
.SetNotNegative()
.SetDisplay("Take Profit %", "Optional take profit distance expressed in percent", "Risk");
_stopLossPercent = Param(nameof(StopLossPercent), 0m)
.SetNotNegative()
.SetDisplay("Stop Loss %", "Optional stop loss distance expressed in percent", "Risk");
Volume = 1m;
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_history.Clear();
_previousEma = null;
_previousPeriod = null;
_currentStats = default;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
// Prepare indicator instances used by the optional filters.
_macd = new MovingAverageConvergenceDivergenceSignal
{
Macd =
{
ShortMa = { Length = MacdFast },
LongMa = { Length = MacdSlow },
},
SignalMa = { Length = MacdSignal }
};
_ema = new EMA { Length = MaPeriod };
var subscription = SubscribeCandles(CandleType);
subscription
.BindEx(_macd, _ema, ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, _ema);
DrawIndicator(area, _macd);
DrawOwnTrades(area);
}
var takeProfit = TakeProfitPercent > 0m ? new Unit(TakeProfitPercent / 100m, UnitTypes.Percent) : new Unit();
var stopLoss = StopLossPercent > 0m ? new Unit(StopLossPercent / 100m, UnitTypes.Percent) : new Unit();
StartProtection(takeProfit, stopLoss);
}
private void ProcessCandle(ICandleMessage candle, IIndicatorValue macdValue, IIndicatorValue emaValue)
{
// Operate only on completed candles.
if (candle.State != CandleStates.Finished)
{
return;
}
// Respect indicator readiness when the corresponding filter is enabled.
MovingAverageConvergenceDivergenceSignalValue macdSignal = null;
if (macdValue.IsFinal)
{
if (macdValue is MovingAverageConvergenceDivergenceSignalValue macdData)
{
macdSignal = macdData;
}
}
else if (EnableMacd)
{
return;
}
decimal? emaSnapshot = null;
if (emaValue.IsFinal)
{
emaSnapshot = emaValue.ToDecimal();
}
else if (EnableMa)
{
return;
}
// Store the candle in the local history used by the probability model.
UpdateHistory(candle);
var candles = _history.ToArray();
_currentStats = FindBestStats(candles);
// Always capture the latest EMA value for slope calculations.
if (emaSnapshot is decimal emaValueDecimal)
{
if (_previousEma == null)
{
_previousEma = emaValueDecimal;
}
}
// Avoid trading before the strategy is fully initialized.
if (!IsFormedAndOnlineAndAllowTrading())
{
if (emaSnapshot is decimal emaValueUnformed)
{
_previousEma = emaValueUnformed;
}
return;
}
if (!_currentStats.IsValid)
{
if (emaSnapshot is decimal emaValueInvalid)
{
_previousEma = emaValueInvalid;
}
return;
}
var flags = CalculateDirection(emaSnapshot, macdSignal);
HandlePositions(flags);
_previousPeriod = _currentStats.Period;
}
private void HandlePositions(DirectionFlags flags)
{
var stats = _currentStats;
// No trades without a valid statistical snapshot.
if (!stats.IsValid)
{
return;
}
// Manage existing positions first to mirror the MQL behaviour.
if (Position > 0)
{
var shouldExitLong = (stats.CurrentDecision == TradeDecisions.Sell &&
stats.SellPossibility >= stats.SellSucPossibilityMid &&
stats.SellSucPossibilityMid > 0m) ||
(flags.DisableBuy && stats.CurrentDecision != TradeDecisions.Buy);
if (shouldExitLong)
{
SellMarket(Position);
return;
}
}
else if (Position < 0)
{
var shouldExitShort = (stats.CurrentDecision == TradeDecisions.Buy &&
stats.BuyPossibility >= stats.BuySucPossibilityMid &&
stats.BuySucPossibilityMid > 0m) ||
(flags.DisableSell && stats.CurrentDecision != TradeDecisions.Sell);
if (shouldExitShort)
{
BuyMarket(-Position);
return;
}
}
// Evaluate fresh entries only when the probability module allows it.
if (stats.CurrentDecision == TradeDecisions.Buy &&
!flags.DisableBuy &&
stats.BuyPossibility >= stats.BuySucPossibilityMid &&
stats.BuySucPossibilityMid > 0m &&
Position <= 0)
{
var volume = Volume + (Position < 0 ? -Position : 0m);
BuyMarket(volume);
return;
}
if (stats.CurrentDecision == TradeDecisions.Sell &&
!flags.DisableSell &&
stats.SellPossibility >= stats.SellSucPossibilityMid &&
stats.SellSucPossibilityMid > 0m &&
Position >= 0)
{
var volume = Volume + (Position > 0 ? Position : 0m);
SellMarket(volume);
}
}
private DirectionFlags CalculateDirection(decimal? emaValue, MovingAverageConvergenceDivergenceSignalValue macdValue)
{
var stats = _currentStats;
var disableBuy = false;
var disableSell = false;
var disablePipsator = false;
var disableBuyPips = false;
var disableSellPips = false;
if (EnableCyberiaLogic)
{
var buyScore = stats.BuyPossibilityMid * stats.BuyPossibilityQuality;
var sellScore = stats.SellPossibilityMid * stats.SellPossibilityQuality;
if (_previousPeriod is int previousPeriodValue)
{
if (stats.Period > previousPeriodValue)
{
if (sellScore > buyScore)
{
disableSell = false;
disableBuy = true;
disableBuyPips = true;
if (stats.SellSucPossibilityMid * stats.SellSucPossibilityQuality >
stats.BuySucPossibilityMid * stats.BuySucPossibilityQuality)
{
disableSell = true;
}
}
else if (sellScore < buyScore)
{
disableSell = true;
disableBuy = false;
disableSellPips = true;
if (stats.SellSucPossibilityMid * stats.SellSucPossibilityQuality <
stats.BuySucPossibilityMid * stats.BuySucPossibilityQuality)
{
disableBuy = true;
}
}
}
else if (stats.Period < previousPeriodValue)
{
disableSell = true;
disableBuy = true;
}
}
if (sellScore == buyScore)
{
disableSell = true;
disableBuy = true;
disablePipsator = false;
}
if (stats.SellPossibility > stats.SellSucPossibilityMid * 2m && stats.SellSucPossibilityMid > 0m)
{
disableSell = true;
disableSellPips = true;
}
if (stats.BuyPossibility > stats.BuySucPossibilityMid * 2m && stats.BuySucPossibilityMid > 0m)
{
disableBuy = true;
disableBuyPips = true;
}
}
if (EnableMa && emaValue is decimal emaDecimal)
{
if (_previousEma is decimal previousEma)
{
if (emaDecimal > previousEma)
{
disableSell = true;
disableSellPips = true;
}
else if (emaDecimal < previousEma)
{
disableBuy = true;
disableBuyPips = true;
}
}
_previousEma = emaDecimal;
}
else if (emaValue is decimal emaSnapshot)
{
_previousEma = emaSnapshot;
}
if (EnableMacd && macdValue != null)
{
var macdMain = macdValue.Macd;
var macdSignal = macdValue.Signal;
if (macdMain > macdSignal)
{
disableSell = true;
}
else if (macdMain < macdSignal)
{
disableBuy = true;
}
}
if (EnableReversalDetector)
{
var trigger = false;
if (stats.BuyPossibilityMid > 0m && stats.BuyPossibility > stats.BuyPossibilityMid * ReversalFactor)
{
trigger = true;
}
if (stats.SellPossibilityMid > 0m && stats.SellPossibility > stats.SellPossibilityMid * ReversalFactor)
{
trigger = true;
}
if (trigger)
{
disableSell = !disableSell;
disableBuy = !disableBuy;
disableSellPips = !disableSellPips;
disableBuyPips = !disableBuyPips;
disablePipsator = !disablePipsator;
}
}
return new DirectionFlags
{
DisableBuy = disableBuy,
DisableSell = disableSell,
DisablePipsator = disablePipsator,
DisableBuyPipsator = disableBuyPips,
DisableSellPipsator = disableSellPips,
};
}
private void UpdateHistory(ICandleMessage candle)
{
var snapshot = new CandleSnapshot(candle.OpenPrice, candle.HighPrice, candle.LowPrice, candle.ClosePrice);
_history.Enqueue(snapshot);
var maxHistory = MaxPeriod * (MaxPeriod * SamplesPerPeriod + 2);
while (_history.Count > maxHistory)
{
_history.Dequeue();
}
}
private ModelStats FindBestStats(CandleSnapshot[] candles)
{
var bestStats = default(ModelStats);
var bestQuality = decimal.MinValue;
var maxPeriod = MaxPeriod;
var segments = SamplesPerPeriod;
var spread = SpreadThreshold;
for (var period = 1; period <= maxPeriod; period++)
{
var modelingBars = period * segments;
var required = period * modelingBars + 1;
if (candles.Length < required)
{
continue;
}
var stats = CalculateStats(candles, period, modelingBars, spread);
if (!stats.IsValid)
{
continue;
}
if (stats.PossibilitySuccessRatio > bestQuality)
{
bestQuality = stats.PossibilitySuccessRatio;
bestStats = stats;
}
}
return bestStats;
}
private ModelStats CalculateStats(CandleSnapshot[] candles, int period, int modelingBars, decimal spreadThreshold)
{
var stats = new ModelStats { Period = period };
var buyQuality = 0;
var sellQuality = 0;
var undefinedQuality = 0;
var buySum = 0m;
var sellSum = 0m;
var undefinedSum = 0m;
var buySuccessSum = 0m;
var sellSuccessSum = 0m;
var undefinedSuccessSum = 0m;
for (var shift = 0; shift < modelingBars; shift++)
{
var currentIndex = candles.Length - 1 - period * shift;
var previousIndex = currentIndex - period;
if (previousIndex < 0)
{
return default;
}
var current = candles[currentIndex];
var previous = candles[previousIndex];
var decisionValue = current.Close - current.Open;
var previousValue = previous.Close - previous.Open;
var buyPossibility = 0m;
var sellPossibility = 0m;
var undefinedPossibility = 0m;
var decision = TradeDecisions.Unknown;
if (decisionValue > 0m)
{
if (previousValue < 0m)
{
decision = TradeDecisions.Sell;
sellPossibility = decisionValue;
}
else
{
undefinedPossibility = decisionValue;
}
}
else if (decisionValue < 0m)
{
if (previousValue > 0m)
{
decision = TradeDecisions.Buy;
buyPossibility = -decisionValue;
}
else
{
undefinedPossibility = -decisionValue;
}
}
if (shift == 0)
{
stats.CurrentDecision = decision;
stats.BuyPossibility = buyPossibility;
stats.SellPossibility = sellPossibility;
stats.UndefinedPossibility = undefinedPossibility;
}
switch (decision)
{
case TradeDecisions.Buy:
buyQuality++;
buySum += buyPossibility;
if (buyPossibility > spreadThreshold)
{
buySuccessSum += buyPossibility;
stats.BuySucPossibilityQuality++;
}
break;
case TradeDecisions.Sell:
sellQuality++;
sellSum += sellPossibility;
if (sellPossibility > spreadThreshold)
{
sellSuccessSum += sellPossibility;
stats.SellSucPossibilityQuality++;
}
break;
default:
undefinedQuality++;
undefinedSum += undefinedPossibility;
if (undefinedPossibility > spreadThreshold)
{
undefinedSuccessSum += undefinedPossibility;
stats.UndefinedSucPossibilityQuality++;
}
break;
}
}
stats.BuyPossibilityQuality = buyQuality;
stats.SellPossibilityQuality = sellQuality;
stats.UndefinedPossibilityQuality = undefinedQuality;
stats.BuyPossibilityMid = buyQuality > 0 ? buySum / buyQuality : 0m;
stats.SellPossibilityMid = sellQuality > 0 ? sellSum / sellQuality : 0m;
stats.UndefinedPossibilityMid = undefinedQuality > 0 ? undefinedSum / undefinedQuality : 0m;
var buySuccessCount = stats.BuySucPossibilityQuality;
var sellSuccessCount = stats.SellSucPossibilityQuality;
var undefinedSuccessCount = stats.UndefinedSucPossibilityQuality;
stats.BuySucPossibilityMid = buySuccessCount > 0 ? buySuccessSum / buySuccessCount : 0m;
stats.SellSucPossibilityMid = sellSuccessCount > 0 ? sellSuccessSum / sellSuccessCount : 0m;
stats.UndefinedSucPossibilityMid = undefinedSuccessCount > 0 ? undefinedSuccessSum / undefinedSuccessCount : 0m;
var successTotal = buySuccessCount + sellSuccessCount + undefinedSuccessCount;
if (successTotal > 0)
{
stats.PossibilitySuccessRatio = (buySuccessCount + sellSuccessCount) / (decimal)successTotal;
}
else
{
stats.PossibilitySuccessRatio = 0m;
}
stats.IsValid = buyQuality + sellQuality + undefinedQuality > 0;
return stats;
}
private readonly struct CandleSnapshot
{
public CandleSnapshot(decimal open, decimal high, decimal low, decimal close)
{
Open = open;
High = high;
Low = low;
Close = close;
}
public decimal Open { get; }
public decimal High { get; }
public decimal Low { get; }
public decimal Close { get; }
}
private struct DirectionFlags
{
public bool DisableBuy;
public bool DisableSell;
public bool DisablePipsator;
public bool DisableBuyPipsator;
public bool DisableSellPipsator;
}
private struct ModelStats
{
public bool IsValid;
public int Period;
public TradeDecisions CurrentDecision;
public decimal BuyPossibility;
public decimal SellPossibility;
public decimal UndefinedPossibility;
public int BuyPossibilityQuality;
public int SellPossibilityQuality;
public int UndefinedPossibilityQuality;
public decimal BuyPossibilityMid;
public decimal SellPossibilityMid;
public decimal UndefinedPossibilityMid;
public decimal BuySucPossibilityMid;
public decimal SellSucPossibilityMid;
public decimal UndefinedSucPossibilityMid;
public int BuySucPossibilityQuality;
public int SellSucPossibilityQuality;
public int UndefinedSucPossibilityQuality;
public decimal PossibilitySuccessRatio;
}
private enum TradeDecisions
{
Unknown,
Buy,
Sell,
}
}
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 collections import deque
from StockSharp.Messages import DataType, CandleStates, Unit, UnitTypes
from StockSharp.Algo.Strategies import Strategy
from StockSharp.Algo.Indicators import (
MovingAverageConvergenceDivergenceSignal,
ExponentialMovingAverage,
)
# Trade decision constants
_DECISION_UNKNOWN = 0
_DECISION_BUY = 1
_DECISION_SELL = 2
class cyberia_trader_ai_strategy(Strategy):
def __init__(self):
super(cyberia_trader_ai_strategy, self).__init__()
self._max_period = self.Param("MaxPeriod", 23) \
.SetDisplay("Max Period", "Largest sampling stride tested by the probability engine", "Model")
self._samples_per_period = self.Param("SamplesPerPeriod", 5) \
.SetDisplay("Segments Per Period", "Number of historical segments processed for every period candidate", "Model")
self._spread_threshold = self.Param("SpreadThreshold", 0.0) \
.SetDisplay("Spread Threshold", "Minimal absolute move to count a probability as successful", "Model")
self._enable_cyberia_logic = self.Param("EnableCyberiaLogic", True) \
.SetDisplay("Enable Cyberia Logic", "Use the probability based disable/allow switches", "Filters")
self._enable_macd = self.Param("EnableMacd", False) \
.SetDisplay("Enable MACD", "Use MACD to block trading against momentum", "Filters")
self._enable_ma = self.Param("EnableMa", False) \
.SetDisplay("Enable EMA", "Use EMA slope to forbid trades against the trend", "Filters")
self._enable_reversal_detector = self.Param("EnableReversalDetector", False) \
.SetDisplay("Enable Reversal Detector", "Flip permissions on extreme probability spikes", "Filters")
self._ma_period = self.Param("MaPeriod", 23) \
.SetDisplay("EMA Period", "Length of the EMA used in the trend filter", "Indicators")
self._macd_fast = self.Param("MacdFast", 12) \
.SetDisplay("MACD Fast", "Fast EMA length for MACD", "Indicators")
self._macd_slow = self.Param("MacdSlow", 26) \
.SetDisplay("MACD Slow", "Slow EMA length for MACD", "Indicators")
self._macd_signal = self.Param("MacdSignal", 9) \
.SetDisplay("MACD Signal", "Signal EMA length for MACD", "Indicators")
self._reversal_factor = self.Param("ReversalFactor", 3.0) \
.SetDisplay("Reversal Factor", "Threshold multiplier that triggers the reversal detector", "Filters")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(2))) \
.SetDisplay("Candle Type", "Primary timeframe processed by the model", "General")
self._take_profit_percent = self.Param("TakeProfitPercent", 0.0) \
.SetDisplay("Take Profit %", "Optional take profit distance expressed in percent", "Risk")
self._stop_loss_percent = self.Param("StopLossPercent", 0.0) \
.SetDisplay("Stop Loss %", "Optional stop loss distance expressed in percent", "Risk")
self._history = deque()
self._previous_ema = None
self._previous_period = None
self._current_stats = None
@property
def MaxPeriod(self):
return self._max_period.Value
@property
def SamplesPerPeriod(self):
return self._samples_per_period.Value
@property
def SpreadThreshold(self):
return self._spread_threshold.Value
@property
def EnableCyberiaLogic(self):
return self._enable_cyberia_logic.Value
@property
def EnableMacd(self):
return self._enable_macd.Value
@property
def EnableMa(self):
return self._enable_ma.Value
@property
def EnableReversalDetector(self):
return self._enable_reversal_detector.Value
@property
def MaPeriod(self):
return self._ma_period.Value
@property
def MacdFast(self):
return self._macd_fast.Value
@property
def MacdSlow(self):
return self._macd_slow.Value
@property
def MacdSignal(self):
return self._macd_signal.Value
@property
def ReversalFactor(self):
return self._reversal_factor.Value
@property
def CandleType(self):
return self._candle_type.Value
@property
def TakeProfitPercent(self):
return self._take_profit_percent.Value
@property
def StopLossPercent(self):
return self._stop_loss_percent.Value
def OnStarted2(self, time):
super(cyberia_trader_ai_strategy, self).OnStarted2(time)
self._macd_indicator = MovingAverageConvergenceDivergenceSignal()
self._macd_indicator.Macd.ShortMa.Length = self.MacdFast
self._macd_indicator.Macd.LongMa.Length = self.MacdSlow
self._macd_indicator.SignalMa.Length = self.MacdSignal
self._ema = ExponentialMovingAverage()
self._ema.Length = self.MaPeriod
subscription = self.SubscribeCandles(self.CandleType)
subscription.BindEx(self._macd_indicator, self._ema, self.ProcessCandle).Start()
tp = float(self.TakeProfitPercent)
sl = float(self.StopLossPercent)
take_profit = Unit(tp / 100.0, UnitTypes.Percent) if tp > 0 else Unit()
stop_loss = Unit(sl / 100.0, UnitTypes.Percent) if sl > 0 else Unit()
self.StartProtection(take_profit, stop_loss)
def ProcessCandle(self, candle, macd_value, ema_value):
if candle.State != CandleStates.Finished:
return
macd_main_val = None
macd_signal_val = None
if macd_value.IsFinal:
if hasattr(macd_value, 'Macd') and hasattr(macd_value, 'Signal'):
m = macd_value.Macd
s = macd_value.Signal
if m is not None:
macd_main_val = float(m)
if s is not None:
macd_signal_val = float(s)
elif self.EnableMacd:
return
ema_snapshot = None
if ema_value.IsFinal:
v = ema_value.Value if hasattr(ema_value, 'Value') else None
if v is not None:
ema_snapshot = float(v)
elif self.EnableMa:
return
self._update_history(candle)
candles = list(self._history)
self._current_stats = self._find_best_stats(candles)
if ema_snapshot is not None:
if self._previous_ema is None:
self._previous_ema = ema_snapshot
if not self.IsFormedAndOnlineAndAllowTrading():
if ema_snapshot is not None:
self._previous_ema = ema_snapshot
return
if self._current_stats is None or not self._current_stats['is_valid']:
if ema_snapshot is not None:
self._previous_ema = ema_snapshot
return
flags = self._calculate_direction(ema_snapshot, macd_main_val, macd_signal_val)
self._handle_positions(flags)
self._previous_period = self._current_stats['period']
def _handle_positions(self, flags):
stats = self._current_stats
if stats is None or not stats['is_valid']:
return
if self.Position > 0:
should_exit = (stats['current_decision'] == _DECISION_SELL and
stats['sell_possibility'] >= stats['sell_suc_possibility_mid'] and
stats['sell_suc_possibility_mid'] > 0) or \
(flags['disable_buy'] and stats['current_decision'] != _DECISION_BUY)
if should_exit:
self.SellMarket(Math.Abs(self.Position))
return
elif self.Position < 0:
should_exit = (stats['current_decision'] == _DECISION_BUY and
stats['buy_possibility'] >= stats['buy_suc_possibility_mid'] and
stats['buy_suc_possibility_mid'] > 0) or \
(flags['disable_sell'] and stats['current_decision'] != _DECISION_SELL)
if should_exit:
self.BuyMarket(Math.Abs(self.Position))
return
if (stats['current_decision'] == _DECISION_BUY and
not flags['disable_buy'] and
stats['buy_possibility'] >= stats['buy_suc_possibility_mid'] and
stats['buy_suc_possibility_mid'] > 0 and
self.Position <= 0):
volume = self.Volume + (Math.Abs(self.Position) if self.Position < 0 else 0)
self.BuyMarket(volume)
return
if (stats['current_decision'] == _DECISION_SELL and
not flags['disable_sell'] and
stats['sell_possibility'] >= stats['sell_suc_possibility_mid'] and
stats['sell_suc_possibility_mid'] > 0 and
self.Position >= 0):
volume = self.Volume + (Math.Abs(self.Position) if self.Position > 0 else 0)
self.SellMarket(volume)
def _calculate_direction(self, ema_val, macd_main, macd_signal):
stats = self._current_stats
disable_buy = False
disable_sell = False
disable_pipsator = False
disable_buy_pips = False
disable_sell_pips = False
if self.EnableCyberiaLogic:
buy_score = stats['buy_possibility_mid'] * stats['buy_possibility_quality']
sell_score = stats['sell_possibility_mid'] * stats['sell_possibility_quality']
if self._previous_period is not None:
if stats['period'] > self._previous_period:
if sell_score > buy_score:
disable_sell = False
disable_buy = True
disable_buy_pips = True
if (stats['sell_suc_possibility_mid'] * stats['sell_suc_possibility_quality'] >
stats['buy_suc_possibility_mid'] * stats['buy_suc_possibility_quality']):
disable_sell = True
elif sell_score < buy_score:
disable_sell = True
disable_buy = False
disable_sell_pips = True
if (stats['sell_suc_possibility_mid'] * stats['sell_suc_possibility_quality'] <
stats['buy_suc_possibility_mid'] * stats['buy_suc_possibility_quality']):
disable_buy = True
elif stats['period'] < self._previous_period:
disable_sell = True
disable_buy = True
if sell_score == buy_score:
disable_sell = True
disable_buy = True
disable_pipsator = False
if stats['sell_suc_possibility_mid'] > 0 and stats['sell_possibility'] > stats['sell_suc_possibility_mid'] * 2:
disable_sell = True
disable_sell_pips = True
if stats['buy_suc_possibility_mid'] > 0 and stats['buy_possibility'] > stats['buy_suc_possibility_mid'] * 2:
disable_buy = True
disable_buy_pips = True
if self.EnableMa and ema_val is not None:
if self._previous_ema is not None:
if ema_val > self._previous_ema:
disable_sell = True
disable_sell_pips = True
elif ema_val < self._previous_ema:
disable_buy = True
disable_buy_pips = True
self._previous_ema = ema_val
elif ema_val is not None:
self._previous_ema = ema_val
if self.EnableMacd and macd_main is not None and macd_signal is not None:
if macd_main > macd_signal:
disable_sell = True
elif macd_main < macd_signal:
disable_buy = True
if self.EnableReversalDetector:
trigger = False
rev_factor = float(self.ReversalFactor)
if stats['buy_possibility_mid'] > 0 and stats['buy_possibility'] > stats['buy_possibility_mid'] * rev_factor:
trigger = True
if stats['sell_possibility_mid'] > 0 and stats['sell_possibility'] > stats['sell_possibility_mid'] * rev_factor:
trigger = True
if trigger:
disable_sell = not disable_sell
disable_buy = not disable_buy
disable_sell_pips = not disable_sell_pips
disable_buy_pips = not disable_buy_pips
disable_pipsator = not disable_pipsator
return {
'disable_buy': disable_buy,
'disable_sell': disable_sell,
'disable_pipsator': disable_pipsator,
'disable_buy_pips': disable_buy_pips,
'disable_sell_pips': disable_sell_pips,
}
def _update_history(self, candle):
snapshot = (float(candle.OpenPrice), float(candle.HighPrice), float(candle.LowPrice), float(candle.ClosePrice))
self._history.append(snapshot)
max_period = self.MaxPeriod
samples = self.SamplesPerPeriod
max_history = max_period * (max_period * samples + 2)
while len(self._history) > max_history:
self._history.popleft()
def _find_best_stats(self, candles):
best_stats = None
best_quality = -1e18
max_period = self.MaxPeriod
segments = self.SamplesPerPeriod
spread = float(self.SpreadThreshold)
for period in range(1, max_period + 1):
modeling_bars = period * segments
required = period * modeling_bars + 1
if len(candles) < required:
continue
stats = self._calculate_stats(candles, period, modeling_bars, spread)
if stats is None or not stats['is_valid']:
continue
if stats['possibility_success_ratio'] > best_quality:
best_quality = stats['possibility_success_ratio']
best_stats = stats
return best_stats
def _calculate_stats(self, candles, period, modeling_bars, spread_threshold):
stats = {
'is_valid': False,
'period': period,
'current_decision': _DECISION_UNKNOWN,
'buy_possibility': 0.0,
'sell_possibility': 0.0,
'undefined_possibility': 0.0,
'buy_possibility_quality': 0,
'sell_possibility_quality': 0,
'undefined_possibility_quality': 0,
'buy_possibility_mid': 0.0,
'sell_possibility_mid': 0.0,
'undefined_possibility_mid': 0.0,
'buy_suc_possibility_mid': 0.0,
'sell_suc_possibility_mid': 0.0,
'undefined_suc_possibility_mid': 0.0,
'buy_suc_possibility_quality': 0,
'sell_suc_possibility_quality': 0,
'undefined_suc_possibility_quality': 0,
'possibility_success_ratio': 0.0,
}
buy_quality = 0
sell_quality = 0
undefined_quality = 0
buy_sum = 0.0
sell_sum = 0.0
undefined_sum = 0.0
buy_success_sum = 0.0
sell_success_sum = 0.0
undefined_success_sum = 0.0
for shift in range(modeling_bars):
current_index = len(candles) - 1 - period * shift
previous_index = current_index - period
if previous_index < 0:
return None
current = candles[current_index]
previous = candles[previous_index]
decision_value = current[3] - current[0] # close - open
previous_value = previous[3] - previous[0]
buy_poss = 0.0
sell_poss = 0.0
undef_poss = 0.0
decision = _DECISION_UNKNOWN
if decision_value > 0:
if previous_value < 0:
decision = _DECISION_SELL
sell_poss = decision_value
else:
undef_poss = decision_value
elif decision_value < 0:
if previous_value > 0:
decision = _DECISION_BUY
buy_poss = -decision_value
else:
undef_poss = -decision_value
if shift == 0:
stats['current_decision'] = decision
stats['buy_possibility'] = buy_poss
stats['sell_possibility'] = sell_poss
stats['undefined_possibility'] = undef_poss
if decision == _DECISION_BUY:
buy_quality += 1
buy_sum += buy_poss
if buy_poss > spread_threshold:
buy_success_sum += buy_poss
stats['buy_suc_possibility_quality'] += 1
elif decision == _DECISION_SELL:
sell_quality += 1
sell_sum += sell_poss
if sell_poss > spread_threshold:
sell_success_sum += sell_poss
stats['sell_suc_possibility_quality'] += 1
else:
undefined_quality += 1
undefined_sum += undef_poss
if undef_poss > spread_threshold:
undefined_success_sum += undef_poss
stats['undefined_suc_possibility_quality'] += 1
stats['buy_possibility_quality'] = buy_quality
stats['sell_possibility_quality'] = sell_quality
stats['undefined_possibility_quality'] = undefined_quality
stats['buy_possibility_mid'] = buy_sum / buy_quality if buy_quality > 0 else 0.0
stats['sell_possibility_mid'] = sell_sum / sell_quality if sell_quality > 0 else 0.0
stats['undefined_possibility_mid'] = undefined_sum / undefined_quality if undefined_quality > 0 else 0.0
bsc = stats['buy_suc_possibility_quality']
ssc = stats['sell_suc_possibility_quality']
usc = stats['undefined_suc_possibility_quality']
stats['buy_suc_possibility_mid'] = buy_success_sum / bsc if bsc > 0 else 0.0
stats['sell_suc_possibility_mid'] = sell_success_sum / ssc if ssc > 0 else 0.0
stats['undefined_suc_possibility_mid'] = undefined_success_sum / usc if usc > 0 else 0.0
success_total = bsc + ssc + usc
if success_total > 0:
stats['possibility_success_ratio'] = (bsc + ssc) / float(success_total)
else:
stats['possibility_success_ratio'] = 0.0
stats['is_valid'] = (buy_quality + sell_quality + undefined_quality) > 0
return stats
def OnReseted(self):
super(cyberia_trader_ai_strategy, self).OnReseted()
self._history = deque()
self._previous_ema = None
self._previous_period = None
self._current_stats = None
def CreateClone(self):
return cyberia_trader_ai_strategy()