Estrategia Adaptativa Perceptron
Descripción General
Esta estrategia es un port a StockSharp del asesor experto de MetaTrader 5 Perceptron.mq5.
Cinco señales discretas de indicadores se combinan a través de un perceptrón de dos capas. Cada operación registra el estado del indicador y, una vez cerrada la posición, los pesos sinápticos se refuerzan o penalizan dependiendo del beneficio obtenido. El comportamiento imita el bucle de autoaprendizaje del EA original aprovechando la API de velas de alto nivel de StockSharp.
Capa de indicadores
| Código | Descripción | Lógica de señal |
|---|---|---|
IND1 |
Cruce de medias móviles simples rápida/lenta | +1 cuando la MA rápida cruza por encima de la MA lenta en la barra anterior, −1 en un cruce descendente, de lo contrario 0. |
IND2 |
Índice de Fuerza Relativa (RSI) | +1 cuando el RSI sale de la zona de sobrevendida (cruza por encima de 30), −1 cuando el RSI sale de la zona de sobrecomprada (cruza por debajo de 70). |
IND3 |
Índice del Canal de Materias Primas (CCI) | +1 en un cruce por encima de −100, −1 en un cruce por debajo de +100. |
IND4 |
Pendiente de la media móvil simple corta | +1 si la MA corta aumentó entre las dos barras anteriores, −1 si disminuyó. |
IND5 |
Color del momentum del Awesome Oscillator | +1 cuando el histograma aumenta en comparación con el valor anterior (color alcista), −1 cuando disminuye. |
Todos los indicadores se evalúan en velas cerradas. Se mantienen búferes históricos internamente para replicar el windowing CopyBuffer utilizado en el script MQL5.
Arquitectura del perceptrón
- Cinco neuronas ocultas (
NN1…NN5) combinan cuatro indicadores cada una, imitando el cableado en el EA. - Cada neurona tiene su propio diccionario de pesos sinápticos más un peso de sesgo (
NNS1…NNS5). - La activación final
brainReturnes la suma ponderada de las salidas de las neuronas.brainReturn > 0→ solicitar una entrada larga (si la operación anterior tampoco fue larga).brainReturn < 0→ solicitar una entrada corta (si la operación anterior tampoco fue corta).
- Las posiciones se abren solo con órdenes de mercado cuando no hay posición activa.
Gestión de posición
- El precio de entrada, dirección y estados de indicador/neurona se capturan en cada ejecución.
- Los desplazamientos de take-profit y stop-loss se aplican en unidades de precio absoluto (p. ej. 0.0004 para 4 puntos en un par Forex con 5 decimales).
Cuando se abre una nueva vela tras la entrada:- Para largos, primero se compara el máximo con el precio de take-profit, luego el mínimo con el stop-loss.
- Para cortos, primero se compara el mínimo con el precio de take-profit, luego el máximo con el stop-loss.
- Si ambos niveles se superan dentro de la misma vela, el take-profit tiene prioridad, coincidiendo con el comportamiento optimista del EA original.
- Una vez detectada una salida, la estrategia cierra la posición con una orden de mercado y calcula el beneficio realizado usando el nivel TP/SL correspondiente.
Actualización adaptativa de pesos
Cuando se cierra una operación, los estados de indicador y neurona capturados se replayan:
- Se determina
directionSign(−1 para largos, +1 para cortos) youtcomeSign(signo del PnL realizado). - Los pesos de sesgo se ajustan dentro de
[SinMin, SinMax]:- Si
sign(neuronOutput) * directionSignes positivo, el sesgo sigue el resultado de la operación (aumenta en ganancias, disminuye en pérdidas). - De lo contrario, el sesgo se mueve en sentido opuesto al resultado.
- Si
- Los pesos sinápticos se comportan de manera similar pero permanecen sin límites: las señales alineadas con la dirección de la posición reciben refuerzo en ganancias y penalizaciones en pérdidas, mientras que las señales opuestas hacen lo inverso.
- Las señales almacenadas se borran para evitar el uso accidental.
Esto generaliza las más de 1.500 líneas de gestión condicional de sinapsis del EA en una rutina de refuerzo compacta.
Parámetros
| Parámetro | Predeterminado | Descripción |
|---|---|---|
CandleType |
Marco temporal de 1 minuto | Suscripción de velas utilizada por la estrategia. |
FastMaLength |
5 | Período de la SMA rápida en la señal de cruce. |
SlowMaLength |
9 | Período de la SMA lenta. |
RsiLength |
14 | Período de cálculo del RSI. |
CciLength |
14 | Período de cálculo del CCI. |
SlopeMaLength |
5 | Período de la MA utilizada para la detección de pendiente. |
AoShortLength |
5 | Período corto del Awesome Oscillator. |
AoLongLength |
34 | Período largo del Awesome Oscillator. |
StopLossOffset |
0.001 | Distancia de stop-loss en unidades de precio absoluto (0 deshabilita el stop). |
TakeProfitOffset |
0.0004 | Distancia de take-profit en unidades de precio absoluto (0 deshabilita el objetivo). |
SinMax |
5 | Límite superior para los pesos de sesgo neuronal. |
SinMin |
0 | Límite inferior para los pesos de sesgo neuronal. |
SinPlusStep |
0.03 | Incremento de refuerzo positivo. |
SinMinusStep |
0.03 | Decremento de refuerzo negativo. |
Todos los parámetros numéricos están expuestos como StrategyParam<T> y pueden optimizarse en StockSharp Designer.
Notas de implementación
- Usa la API de suscripción de velas de alto nivel con vinculación multi-indicador.
- Se emplea gestión manual de operaciones para que los precios realizados sean conocidos al actualizar las sinapsis.
- Los historiales de indicadores se almacenan con campos anulables para asegurar que las señales solo se activen después de la formación completa.
- El búfer de color del Awesome Oscillator en el EA se aproxima comparando los valores actuales y anteriores del histograma.
- La salida de gráfico dibuja la serie de velas más las medias móviles rápida y lenta. Los marcadores de operaciones muestran el comportamiento adaptativo en tiempo real.
Limitaciones y supuestos
- Los stops y objetivos se evalúan una vez por vela completada; el orden intrabar de los eventos es desconocido, por lo que se da prioridad al objetivo de ganancia cuando se alcanzan ambos umbrales.
- Los pesos de indicadores no están acotados como en el EA original y pueden crecer considerablemente durante ciclos de refuerzo prolongados.
- El
LastTradeTypedel EA original nunca se reiniciaba; en este port se borra después de cada salida para que las operaciones consecutivas en la misma dirección sigan siendo posibles.
using System;
using System.Linq;
using System.Collections.Generic;
using Ecng.Common;
using Ecng.Collections;
using Ecng.Serialization;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Adaptive multi-layer perceptron strategy converted from the MetaTrader 5 "Perceptron" expert advisor.
/// Combines five discrete indicator signals and tunes their synaptic weights after every completed trade.
/// </summary>
public class PerceptronAdaptiveStrategy : Strategy
{
private readonly StrategyParam<decimal> _stopLossOffset;
private readonly StrategyParam<decimal> _takeProfitOffset;
private readonly StrategyParam<int> _sinMax;
private readonly StrategyParam<int> _sinMin;
private readonly StrategyParam<decimal> _sinPlus;
private readonly StrategyParam<decimal> _sinMinus;
private readonly StrategyParam<int> _fastMaLength;
private readonly StrategyParam<int> _slowMaLength;
private readonly StrategyParam<int> _rsiLength;
private readonly StrategyParam<int> _cciLength;
private readonly StrategyParam<int> _slopeMaLength;
private readonly StrategyParam<int> _aoShortLength;
private readonly StrategyParam<int> _aoLongLength;
private readonly StrategyParam<DataType> _candleType;
private decimal[] _baseWeights = new decimal[5];
// Use a flat 5x6 array instead of Dictionary[] to avoid clone validation issues.
// Index: [neuronIndex, indicatorIndex] where indicatorIndex is 1-5 (index 0 unused).
private decimal[,] _indicatorWeights = new decimal[5, 6];
private static readonly int[][] _neuronIndicators =
{
new[] { 2, 3, 4, 5 },
new[] { 1, 3, 4, 5 },
new[] { 1, 2, 4, 5 },
new[] { 1, 2, 3, 5 },
new[] { 1, 2, 3, 4 },
};
private int[] _lastIndicatorSignals = new int[5];
private decimal[] _lastNeuronOutputs = new decimal[5];
private SimpleMovingAverage _fastMa = null!;
private SimpleMovingAverage _slowMa = null!;
private RelativeStrengthIndex _rsi = null!;
private CommodityChannelIndex _cci = null!;
private SimpleMovingAverage _slopeMa = null!;
private AwesomeOscillator _ao = null!;
private decimal? _prevFastMa;
private decimal? _prevPrevFastMa;
private decimal? _prevSlowMa;
private decimal? _prevRsi;
private decimal? _prevPrevRsi;
private decimal? _prevCci;
private decimal? _prevPrevCci;
private decimal? _prevSlopeMa;
private decimal? _prevPrevSlopeMa;
private decimal? _prevAo;
private bool _hasLastSignals;
private int _lastTradeDirection;
private decimal _entryPrice;
private decimal _stopLossPrice;
private decimal _takeProfitPrice;
private bool _isLongPosition;
private DateTimeOffset? _entryCandleTime;
/// <summary>
/// Stop-loss distance in absolute price units.
/// </summary>
public decimal StopLossOffset
{
get => _stopLossOffset.Value;
set => _stopLossOffset.Value = value;
}
/// <summary>
/// Take-profit distance in absolute price units.
/// </summary>
public decimal TakeProfitOffset
{
get => _takeProfitOffset.Value;
set => _takeProfitOffset.Value = value;
}
/// <summary>
/// Upper boundary for neuron bias weights.
/// </summary>
public int SinMax
{
get => _sinMax.Value;
set => _sinMax.Value = value;
}
/// <summary>
/// Lower boundary for neuron bias weights.
/// </summary>
public int SinMin
{
get => _sinMin.Value;
set => _sinMin.Value = value;
}
/// <summary>
/// Increment applied when reinforcing synaptic weights.
/// </summary>
public decimal SinPlusStep
{
get => _sinPlus.Value;
set => _sinPlus.Value = value;
}
/// <summary>
/// Decrement applied when penalizing synaptic weights.
/// </summary>
public decimal SinMinusStep
{
get => _sinMinus.Value;
set => _sinMinus.Value = value;
}
/// <summary>
/// Length of the fast moving average used in the crossover signal.
/// </summary>
public int FastMaLength
{
get => _fastMaLength.Value;
set => _fastMaLength.Value = value;
}
/// <summary>
/// Length of the slow moving average used in the crossover signal.
/// </summary>
public int SlowMaLength
{
get => _slowMaLength.Value;
set => _slowMaLength.Value = value;
}
/// <summary>
/// RSI lookback length.
/// </summary>
public int RsiLength
{
get => _rsiLength.Value;
set => _rsiLength.Value = value;
}
/// <summary>
/// CCI lookback length.
/// </summary>
public int CciLength
{
get => _cciLength.Value;
set => _cciLength.Value = value;
}
/// <summary>
/// Length of the smoothing average used for the trend slope signal.
/// </summary>
public int SlopeMaLength
{
get => _slopeMaLength.Value;
set => _slopeMaLength.Value = value;
}
/// <summary>
/// Short period of the Awesome Oscillator.
/// </summary>
public int AoShortLength
{
get => _aoShortLength.Value;
set => _aoShortLength.Value = value;
}
/// <summary>
/// Long period of the Awesome Oscillator.
/// </summary>
public int AoLongLength
{
get => _aoLongLength.Value;
set => _aoLongLength.Value = value;
}
/// <summary>
/// Candle type used for calculations.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initialize <see cref="PerceptronAdaptiveStrategy"/>.
/// </summary>
public PerceptronAdaptiveStrategy()
{
_stopLossOffset = Param(nameof(StopLossOffset), 500m)
.SetNotNegative()
.SetDisplay("Stop Loss Offset", "Stop-loss distance in absolute price units", "Risk Management")
.SetOptimize(0.0005m, 0.005m, 0.0005m);
_takeProfitOffset = Param(nameof(TakeProfitOffset), 300m)
.SetNotNegative()
.SetDisplay("Take Profit Offset", "Take-profit distance in absolute price units", "Risk Management")
.SetOptimize(0.0004m, 0.006m, 0.0004m);
_sinMax = Param(nameof(SinMax), 5)
.SetDisplay("Synapse Upper Bound", "Maximum value for neuron bias weights", "Neural Network")
.SetOptimize(3, 10, 1);
_sinMin = Param(nameof(SinMin), 0)
.SetDisplay("Synapse Lower Bound", "Minimum value for neuron bias weights", "Neural Network")
.SetOptimize(-5, 0, 1);
_sinPlus = Param(nameof(SinPlusStep), 0.03m)
.SetGreaterThanZero()
.SetDisplay("Positive Adjustment", "Increment applied when trade result is favorable", "Neural Network")
.SetOptimize(0.01m, 0.1m, 0.01m);
_sinMinus = Param(nameof(SinMinusStep), 0.03m)
.SetGreaterThanZero()
.SetDisplay("Negative Adjustment", "Decrement applied when trade result is unfavorable", "Neural Network")
.SetOptimize(0.01m, 0.1m, 0.01m);
_fastMaLength = Param(nameof(FastMaLength), 5)
.SetGreaterThanZero()
.SetDisplay("Fast MA Length", "Fast simple moving average length", "Indicators")
.SetOptimize(3, 20, 1);
_slowMaLength = Param(nameof(SlowMaLength), 9)
.SetGreaterThanZero()
.SetDisplay("Slow MA Length", "Slow simple moving average length", "Indicators")
.SetOptimize(5, 40, 1);
_rsiLength = Param(nameof(RsiLength), 14)
.SetGreaterThanZero()
.SetDisplay("RSI Length", "Relative Strength Index period", "Indicators")
.SetOptimize(7, 30, 1);
_cciLength = Param(nameof(CciLength), 14)
.SetGreaterThanZero()
.SetDisplay("CCI Length", "Commodity Channel Index period", "Indicators")
.SetOptimize(7, 40, 1);
_slopeMaLength = Param(nameof(SlopeMaLength), 5)
.SetGreaterThanZero()
.SetDisplay("Slope MA Length", "Simple moving average used for slope detection", "Indicators")
.SetOptimize(3, 20, 1);
_aoShortLength = Param(nameof(AoShortLength), 5)
.SetGreaterThanZero()
.SetDisplay("AO Short Length", "Short period for the Awesome Oscillator", "Indicators")
.SetOptimize(3, 10, 1);
_aoLongLength = Param(nameof(AoLongLength), 34)
.SetGreaterThanZero()
.SetDisplay("AO Long Length", "Long period for the Awesome Oscillator", "Indicators")
.SetOptimize(20, 60, 1);
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
.SetDisplay("Candle Type", "Timeframe used for calculations", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
ResetState();
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
ResetState();
_fastMa = new SMA { Length = FastMaLength };
_slowMa = new SMA { Length = SlowMaLength };
_rsi = new RelativeStrengthIndex { Length = RsiLength };
_cci = new CommodityChannelIndex { Length = CciLength };
_slopeMa = new SMA { Length = SlopeMaLength };
_ao = new AwesomeOscillator
{
ShortMa = { Length = AoShortLength },
LongMa = { Length = AoLongLength },
};
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(_fastMa, _slowMa, _rsi, _cci, _slopeMa, _ao, ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, _fastMa);
DrawIndicator(area, _slowMa);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle, decimal fastMaValue, decimal slowMaValue, decimal rsiValue, decimal cciValue, decimal slopeMaValue, decimal aoValue)
{
if (candle.State != CandleStates.Finished)
return;
var maSignal = UpdateMaSignal(fastMaValue, slowMaValue);
var rsiSignal = UpdateRsiSignal(rsiValue);
var cciSignal = UpdateCciSignal(cciValue);
var slopeSignal = UpdateSlopeSignal(slopeMaValue);
var aoSignal = UpdateAoSignal(aoValue);
HandlePositionManagement(candle);
if (!_fastMa.IsFormed || !_slowMa.IsFormed || !_rsi.IsFormed || !_cci.IsFormed)
return;
if (Position != 0)
return;
var indicatorSignals = new[] { maSignal, rsiSignal, cciSignal, slopeSignal, aoSignal };
var neuronOutputs = CalculateNeuronOutputs(indicatorSignals);
var brainReturn = CalculateBrainReturn(neuronOutputs);
if (brainReturn > 0m && _lastTradeDirection != 2)
{
OpenPosition(true, candle.ClosePrice, candle.OpenTime, indicatorSignals, neuronOutputs);
}
else if (brainReturn < 0m && _lastTradeDirection != 1)
{
OpenPosition(false, candle.ClosePrice, candle.OpenTime, indicatorSignals, neuronOutputs);
}
}
private void OpenPosition(bool isLong, decimal entryPrice, DateTimeOffset candleOpenTime, IReadOnlyList<int> indicatorSignals, IReadOnlyList<decimal> neuronOutputs)
{
var volume = Volume;
if (isLong)
{
BuyMarket();
_lastTradeDirection = 2;
}
else
{
SellMarket();
_lastTradeDirection = 1;
}
_entryPrice = entryPrice;
_isLongPosition = isLong;
_entryCandleTime = candleOpenTime;
var stopOffset = StopLossOffset;
var takeOffset = TakeProfitOffset;
_stopLossPrice = 0m;
_takeProfitPrice = 0m;
if (stopOffset > 0m)
{
_stopLossPrice = isLong ? entryPrice - stopOffset : entryPrice + stopOffset;
}
if (takeOffset > 0m)
{
_takeProfitPrice = isLong ? entryPrice + takeOffset : entryPrice - takeOffset;
}
_hasLastSignals = true;
for (var i = 0; i < indicatorSignals.Count; ++i)
_lastIndicatorSignals[i] = indicatorSignals[i];
for (var i = 0; i < neuronOutputs.Count; ++i)
_lastNeuronOutputs[i] = neuronOutputs[i];
}
private void HandlePositionManagement(ICandleMessage candle)
{
if (Position == 0 || _entryCandleTime is null)
return;
if (candle.OpenTime <= _entryCandleTime.Value)
return;
var hasExit = false;
decimal exitPrice = 0m;
if (_isLongPosition)
{
if (_takeProfitPrice > 0m && candle.HighPrice >= _takeProfitPrice)
{
exitPrice = _takeProfitPrice;
hasExit = true;
}
else if (_stopLossPrice > 0m && candle.LowPrice <= _stopLossPrice)
{
exitPrice = _stopLossPrice;
hasExit = true;
}
}
else
{
if (_takeProfitPrice > 0m && candle.LowPrice <= _takeProfitPrice)
{
exitPrice = _takeProfitPrice;
hasExit = true;
}
else if (_stopLossPrice > 0m && candle.HighPrice >= _stopLossPrice)
{
exitPrice = _stopLossPrice;
hasExit = true;
}
}
if (!hasExit)
return;
if (Position > 0)
SellMarket();
else if (Position < 0)
BuyMarket();
var profit = _isLongPosition ? exitPrice - _entryPrice : _entryPrice - exitPrice;
if (_hasLastSignals)
{
AdjustWeights(_isLongPosition, profit);
}
ResetAfterExit();
}
private void AdjustWeights(bool wasLongTrade, decimal profit)
{
var outcomeSign = Math.Sign(profit);
if (outcomeSign == 0)
return;
var directionSign = wasLongTrade ? -1 : 1;
var sinPlus = SinPlusStep;
var sinMinus = SinMinusStep;
var sinMax = (decimal)SinMax;
var sinMin = (decimal)SinMin;
for (var neuronIndex = 0; neuronIndex < _baseWeights.Length; neuronIndex++)
{
var lastOutput = _lastNeuronOutputs[neuronIndex];
var neuronSign = Math.Sign(lastOutput);
if (neuronSign != 0)
{
var product = neuronSign * directionSign;
if (product > 0)
{
if (outcomeSign > 0)
{
_baseWeights[neuronIndex] = Math.Min(_baseWeights[neuronIndex] + sinPlus, sinMax);
}
else
{
_baseWeights[neuronIndex] = Math.Max(_baseWeights[neuronIndex] - sinMinus, sinMin);
}
}
else if (product < 0)
{
if (outcomeSign > 0)
{
_baseWeights[neuronIndex] = Math.Max(_baseWeights[neuronIndex] - sinMinus, sinMin);
}
else
{
_baseWeights[neuronIndex] = Math.Min(_baseWeights[neuronIndex] + sinPlus, sinMax);
}
}
}
foreach (var indicatorIndex in _neuronIndicators[neuronIndex])
{
var indicatorSignal = _lastIndicatorSignals[indicatorIndex - 1];
if (indicatorSignal == 0)
continue;
var product = indicatorSignal * directionSign;
if (product > 0)
{
_indicatorWeights[neuronIndex, indicatorIndex] += outcomeSign > 0 ? sinPlus : -sinMinus;
}
else if (product < 0)
{
_indicatorWeights[neuronIndex, indicatorIndex] += outcomeSign > 0 ? -sinMinus : sinPlus;
}
}
}
}
private decimal[] CalculateNeuronOutputs(IReadOnlyList<int> indicatorSignals)
{
var outputs = new decimal[_baseWeights.Length];
for (var neuronIndex = 0; neuronIndex < outputs.Length; neuronIndex++)
{
var sum = 0m;
foreach (var indicatorIndex in _neuronIndicators[neuronIndex])
{
var signal = indicatorSignals[indicatorIndex - 1];
if (signal == 0)
continue;
var weight = _indicatorWeights[neuronIndex, indicatorIndex];
sum += weight * signal;
}
outputs[neuronIndex] = sum;
}
return outputs;
}
private decimal CalculateBrainReturn(IReadOnlyList<decimal> neuronOutputs)
{
var total = 0m;
for (var i = 0; i < neuronOutputs.Count; ++i)
total += neuronOutputs[i] * _baseWeights[i];
return total;
}
private int UpdateMaSignal(decimal fastMaValue, decimal slowMaValue)
{
if (!_fastMa.IsFormed || !_slowMa.IsFormed)
{
_prevPrevFastMa = _prevFastMa;
_prevFastMa = fastMaValue;
_prevSlowMa = slowMaValue;
return 0;
}
if (_prevFastMa is null || _prevPrevFastMa is null || _prevSlowMa is null)
{
_prevPrevFastMa = _prevFastMa;
_prevFastMa = fastMaValue;
_prevSlowMa = slowMaValue;
return 0;
}
var previousFast = _prevFastMa.Value;
var previousFast2 = _prevPrevFastMa.Value;
var previousSlow = _prevSlowMa.Value;
var signal = 0;
if (previousFast2 < previousSlow && previousFast > previousSlow)
signal = 1;
else if (previousFast2 > previousSlow && previousFast < previousSlow)
signal = -1;
_prevPrevFastMa = _prevFastMa;
_prevFastMa = fastMaValue;
_prevSlowMa = slowMaValue;
return signal;
}
private int UpdateRsiSignal(decimal rsiValue)
{
if (!_rsi.IsFormed)
{
_prevPrevRsi = _prevRsi;
_prevRsi = rsiValue;
return 0;
}
if (_prevRsi is null || _prevPrevRsi is null)
{
_prevPrevRsi = _prevRsi;
_prevRsi = rsiValue;
return 0;
}
var previous = _prevRsi.Value;
var previous2 = _prevPrevRsi.Value;
var signal = 0;
if (previous2 < 30m && previous > 30m)
signal = 1;
else if (previous2 > 70m && previous < 70m)
signal = -1;
_prevPrevRsi = _prevRsi;
_prevRsi = rsiValue;
return signal;
}
private int UpdateCciSignal(decimal cciValue)
{
if (!_cci.IsFormed)
{
_prevPrevCci = _prevCci;
_prevCci = cciValue;
return 0;
}
if (_prevCci is null || _prevPrevCci is null)
{
_prevPrevCci = _prevCci;
_prevCci = cciValue;
return 0;
}
var previous = _prevCci.Value;
var previous2 = _prevPrevCci.Value;
var signal = 0;
if (previous2 < -100m && previous > -100m)
signal = 1;
else if (previous2 > 100m && previous < 100m)
signal = -1;
_prevPrevCci = _prevCci;
_prevCci = cciValue;
return signal;
}
private int UpdateSlopeSignal(decimal slopeValue)
{
if (!_slopeMa.IsFormed)
{
_prevPrevSlopeMa = _prevSlopeMa;
_prevSlopeMa = slopeValue;
return 0;
}
if (_prevSlopeMa is null || _prevPrevSlopeMa is null)
{
_prevPrevSlopeMa = _prevSlopeMa;
_prevSlopeMa = slopeValue;
return 0;
}
var previous = _prevSlopeMa.Value;
var previous2 = _prevPrevSlopeMa.Value;
var signal = 0;
if (previous > previous2)
signal = 1;
else if (previous < previous2)
signal = -1;
_prevPrevSlopeMa = _prevSlopeMa;
_prevSlopeMa = slopeValue;
return signal;
}
private int UpdateAoSignal(decimal aoValue)
{
if (!_ao.IsFormed)
{
_prevAo = aoValue;
return 0;
}
if (_prevAo is null)
{
_prevAo = aoValue;
return 0;
}
var previous = _prevAo.Value;
var signal = 0;
if (aoValue > previous)
signal = 1;
else if (aoValue < previous)
signal = -1;
_prevAo = aoValue;
return signal;
}
private void ResetState()
{
_baseWeights = new decimal[5];
_lastIndicatorSignals = new int[5];
_lastNeuronOutputs = new decimal[5];
_indicatorWeights = new decimal[5, 6];
for (var i = 0; i < _baseWeights.Length; ++i)
_baseWeights[i] = 1m;
for (var i = 0; i < 5; ++i)
{
foreach (var indicatorIndex in _neuronIndicators[i])
_indicatorWeights[i, indicatorIndex] = 1m;
}
_prevFastMa = null;
_prevPrevFastMa = null;
_prevSlowMa = null;
_prevRsi = null;
_prevPrevRsi = null;
_prevCci = null;
_prevPrevCci = null;
_prevSlopeMa = null;
_prevPrevSlopeMa = null;
_prevAo = null;
_hasLastSignals = false;
_lastTradeDirection = 0;
_entryPrice = 0m;
_stopLossPrice = 0m;
_takeProfitPrice = 0m;
_isLongPosition = false;
_entryCandleTime = null;
}
private void ResetAfterExit()
{
_entryPrice = 0m;
_stopLossPrice = 0m;
_takeProfitPrice = 0m;
_isLongPosition = false;
_entryCandleTime = null;
_lastTradeDirection = 0;
_hasLastSignals = false;
_lastIndicatorSignals = new int[5];
_lastNeuronOutputs = new decimal[5];
}
}
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.Strategies import Strategy
from StockSharp.Algo.Indicators import (
SimpleMovingAverage,
RelativeStrengthIndex,
CommodityChannelIndex,
AwesomeOscillator,
)
class perceptron_adaptive_strategy(Strategy):
"""Adaptive multi-layer perceptron: combines 5 indicator signals, tunes weights after trades."""
# Neuron-to-indicator mapping (each neuron uses 4 of 5 indicators, indices 1-5)
_NEURON_INDICATORS = [
[2, 3, 4, 5],
[1, 3, 4, 5],
[1, 2, 4, 5],
[1, 2, 3, 5],
[1, 2, 3, 4],
]
def __init__(self):
super(perceptron_adaptive_strategy, self).__init__()
self._stop_loss_offset = self.Param("StopLossOffset", 500.0) \
.SetDisplay("Stop Loss Offset", "Stop-loss distance in absolute price units", "Risk Management")
self._take_profit_offset = self.Param("TakeProfitOffset", 300.0) \
.SetDisplay("Take Profit Offset", "Take-profit distance in absolute price units", "Risk Management")
self._sin_max = self.Param("SinMax", 5) \
.SetDisplay("Synapse Upper Bound", "Maximum value for neuron bias weights", "Neural Network")
self._sin_min = self.Param("SinMin", 0) \
.SetDisplay("Synapse Lower Bound", "Minimum value for neuron bias weights", "Neural Network")
self._sin_plus = self.Param("SinPlusStep", 0.03) \
.SetGreaterThanZero() \
.SetDisplay("Positive Adjustment", "Increment applied when trade is favorable", "Neural Network")
self._sin_minus = self.Param("SinMinusStep", 0.03) \
.SetGreaterThanZero() \
.SetDisplay("Negative Adjustment", "Decrement applied when trade is unfavorable", "Neural Network")
self._fast_ma_length = self.Param("FastMaLength", 5) \
.SetGreaterThanZero() \
.SetDisplay("Fast MA Length", "Fast simple moving average length", "Indicators")
self._slow_ma_length = self.Param("SlowMaLength", 9) \
.SetGreaterThanZero() \
.SetDisplay("Slow MA Length", "Slow simple moving average length", "Indicators")
self._rsi_length = self.Param("RsiLength", 14) \
.SetGreaterThanZero() \
.SetDisplay("RSI Length", "Relative Strength Index period", "Indicators")
self._cci_length = self.Param("CciLength", 14) \
.SetGreaterThanZero() \
.SetDisplay("CCI Length", "Commodity Channel Index period", "Indicators")
self._slope_ma_length = self.Param("SlopeMaLength", 5) \
.SetGreaterThanZero() \
.SetDisplay("Slope MA Length", "SMA for slope detection", "Indicators")
self._ao_short_length = self.Param("AoShortLength", 5) \
.SetGreaterThanZero() \
.SetDisplay("AO Short Length", "Short period for Awesome Oscillator", "Indicators")
self._ao_long_length = self.Param("AoLongLength", 34) \
.SetGreaterThanZero() \
.SetDisplay("AO Long Length", "Long period for Awesome Oscillator", "Indicators")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4))) \
.SetDisplay("Candle Type", "Timeframe used for calculations", "General")
self._base_weights = [1.0] * 5
# indicator_weights[neuron][indicator_index 0..5]
self._indicator_weights = [[0.0] * 6 for _ in range(5)]
self._last_indicator_signals = [0] * 5
self._last_neuron_outputs = [0.0] * 5
self._prev_fast_ma = None
self._prev_prev_fast_ma = None
self._prev_slow_ma = None
self._prev_rsi = None
self._prev_prev_rsi = None
self._prev_cci = None
self._prev_prev_cci = None
self._prev_slope_ma = None
self._prev_prev_slope_ma = None
self._prev_ao = None
self._has_last_signals = False
self._last_trade_direction = 0
self._entry_price = 0.0
self._stop_loss_price = 0.0
self._take_profit_price = 0.0
self._is_long_position = False
self._entry_candle_time = None
@property
def StopLossOffset(self):
return float(self._stop_loss_offset.Value)
@property
def TakeProfitOffset(self):
return float(self._take_profit_offset.Value)
@property
def SinMax(self):
return int(self._sin_max.Value)
@property
def SinMin(self):
return int(self._sin_min.Value)
@property
def SinPlusStep(self):
return float(self._sin_plus.Value)
@property
def SinMinusStep(self):
return float(self._sin_minus.Value)
@property
def FastMaLength(self):
return int(self._fast_ma_length.Value)
@property
def SlowMaLength(self):
return int(self._slow_ma_length.Value)
@property
def RsiLength(self):
return int(self._rsi_length.Value)
@property
def CciLength(self):
return int(self._cci_length.Value)
@property
def SlopeMaLength(self):
return int(self._slope_ma_length.Value)
@property
def AoShortLength(self):
return int(self._ao_short_length.Value)
@property
def AoLongLength(self):
return int(self._ao_long_length.Value)
@property
def CandleType(self):
return self._candle_type.Value
def _reset_state(self):
self._base_weights = [1.0] * 5
self._indicator_weights = [[0.0] * 6 for _ in range(5)]
for i in range(5):
for idx in self._NEURON_INDICATORS[i]:
self._indicator_weights[i][idx] = 1.0
self._last_indicator_signals = [0] * 5
self._last_neuron_outputs = [0.0] * 5
self._prev_fast_ma = None
self._prev_prev_fast_ma = None
self._prev_slow_ma = None
self._prev_rsi = None
self._prev_prev_rsi = None
self._prev_cci = None
self._prev_prev_cci = None
self._prev_slope_ma = None
self._prev_prev_slope_ma = None
self._prev_ao = None
self._has_last_signals = False
self._last_trade_direction = 0
self._entry_price = 0.0
self._stop_loss_price = 0.0
self._take_profit_price = 0.0
self._is_long_position = False
self._entry_candle_time = None
def OnStarted2(self, time):
super(perceptron_adaptive_strategy, self).OnStarted2(time)
self._reset_state()
self._fast_ma = SimpleMovingAverage()
self._fast_ma.Length = self.FastMaLength
self._slow_ma = SimpleMovingAverage()
self._slow_ma.Length = self.SlowMaLength
self._rsi = RelativeStrengthIndex()
self._rsi.Length = self.RsiLength
self._cci = CommodityChannelIndex()
self._cci.Length = self.CciLength
self._slope_ma = SimpleMovingAverage()
self._slope_ma.Length = self.SlopeMaLength
self._ao = AwesomeOscillator()
self._ao.ShortMa.Length = self.AoShortLength
self._ao.LongMa.Length = self.AoLongLength
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self._fast_ma, self._slow_ma, self._rsi, self._cci, self._slope_ma, self._ao, self.process_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, self._fast_ma)
self.DrawIndicator(area, self._slow_ma)
self.DrawOwnTrades(area)
def process_candle(self, candle, fast_ma_val, slow_ma_val, rsi_val, cci_val, slope_ma_val, ao_val):
if candle.State != CandleStates.Finished:
return
fast_ma_v = float(fast_ma_val)
slow_ma_v = float(slow_ma_val)
rsi_v = float(rsi_val)
cci_v = float(cci_val)
slope_v = float(slope_ma_val)
ao_v = float(ao_val)
ma_signal = self._update_ma_signal(fast_ma_v, slow_ma_v)
rsi_signal = self._update_rsi_signal(rsi_v)
cci_signal = self._update_cci_signal(cci_v)
slope_signal = self._update_slope_signal(slope_v)
ao_signal = self._update_ao_signal(ao_v)
self._handle_position_management(candle)
if (not self._fast_ma.IsFormed or not self._slow_ma.IsFormed
or not self._rsi.IsFormed or not self._cci.IsFormed):
return
if self.Position != 0:
return
indicator_signals = [ma_signal, rsi_signal, cci_signal, slope_signal, ao_signal]
neuron_outputs = self._calculate_neuron_outputs(indicator_signals)
brain_return = self._calculate_brain_return(neuron_outputs)
if brain_return > 0 and self._last_trade_direction != 2:
self._open_position(True, float(candle.ClosePrice), candle.OpenTime, indicator_signals, neuron_outputs)
elif brain_return < 0 and self._last_trade_direction != 1:
self._open_position(False, float(candle.ClosePrice), candle.OpenTime, indicator_signals, neuron_outputs)
def _open_position(self, is_long, entry_price, candle_time, indicator_signals, neuron_outputs):
if is_long:
self.BuyMarket()
self._last_trade_direction = 2
else:
self.SellMarket()
self._last_trade_direction = 1
self._entry_price = entry_price
self._is_long_position = is_long
self._entry_candle_time = candle_time
stop_offset = self.StopLossOffset
take_offset = self.TakeProfitOffset
self._stop_loss_price = 0.0
self._take_profit_price = 0.0
if stop_offset > 0:
self._stop_loss_price = entry_price - stop_offset if is_long else entry_price + stop_offset
if take_offset > 0:
self._take_profit_price = entry_price + take_offset if is_long else entry_price - take_offset
self._has_last_signals = True
for i in range(len(indicator_signals)):
self._last_indicator_signals[i] = indicator_signals[i]
for i in range(len(neuron_outputs)):
self._last_neuron_outputs[i] = neuron_outputs[i]
def _handle_position_management(self, candle):
if self.Position == 0 or self._entry_candle_time is None:
return
if candle.OpenTime <= self._entry_candle_time:
return
has_exit = False
exit_price = 0.0
h = float(candle.HighPrice)
lo = float(candle.LowPrice)
if self._is_long_position:
if self._take_profit_price > 0 and h >= self._take_profit_price:
exit_price = self._take_profit_price
has_exit = True
elif self._stop_loss_price > 0 and lo <= self._stop_loss_price:
exit_price = self._stop_loss_price
has_exit = True
else:
if self._take_profit_price > 0 and lo <= self._take_profit_price:
exit_price = self._take_profit_price
has_exit = True
elif self._stop_loss_price > 0 and h >= self._stop_loss_price:
exit_price = self._stop_loss_price
has_exit = True
if not has_exit:
return
if self.Position > 0:
self.SellMarket()
elif self.Position < 0:
self.BuyMarket()
profit = exit_price - self._entry_price if self._is_long_position else self._entry_price - exit_price
if self._has_last_signals:
self._adjust_weights(self._is_long_position, profit)
self._reset_after_exit()
def _adjust_weights(self, was_long, profit):
if profit > 0:
outcome_sign = 1
elif profit < 0:
outcome_sign = -1
else:
return
direction_sign = -1 if was_long else 1
sin_plus = self.SinPlusStep
sin_minus = self.SinMinusStep
sin_max = float(self.SinMax)
sin_min = float(self.SinMin)
for ni in range(5):
last_output = self._last_neuron_outputs[ni]
if last_output > 0:
neuron_sign = 1
elif last_output < 0:
neuron_sign = -1
else:
neuron_sign = 0
if neuron_sign != 0:
product = neuron_sign * direction_sign
if product > 0:
if outcome_sign > 0:
self._base_weights[ni] = min(self._base_weights[ni] + sin_plus, sin_max)
else:
self._base_weights[ni] = max(self._base_weights[ni] - sin_minus, sin_min)
elif product < 0:
if outcome_sign > 0:
self._base_weights[ni] = max(self._base_weights[ni] - sin_minus, sin_min)
else:
self._base_weights[ni] = min(self._base_weights[ni] + sin_plus, sin_max)
for ind_idx in self._NEURON_INDICATORS[ni]:
ind_signal = self._last_indicator_signals[ind_idx - 1]
if ind_signal == 0:
continue
product = ind_signal * direction_sign
if product > 0:
self._indicator_weights[ni][ind_idx] += sin_plus if outcome_sign > 0 else -sin_minus
elif product < 0:
self._indicator_weights[ni][ind_idx] += -sin_minus if outcome_sign > 0 else sin_plus
def _calculate_neuron_outputs(self, indicator_signals):
outputs = [0.0] * 5
for ni in range(5):
s = 0.0
for ind_idx in self._NEURON_INDICATORS[ni]:
sig = indicator_signals[ind_idx - 1]
if sig == 0:
continue
w = self._indicator_weights[ni][ind_idx]
s += w * sig
outputs[ni] = s
return outputs
def _calculate_brain_return(self, neuron_outputs):
total = 0.0
for i in range(len(neuron_outputs)):
total += neuron_outputs[i] * self._base_weights[i]
return total
def _update_ma_signal(self, fast_val, slow_val):
if not self._fast_ma.IsFormed or not self._slow_ma.IsFormed:
self._prev_prev_fast_ma = self._prev_fast_ma
self._prev_fast_ma = fast_val
self._prev_slow_ma = slow_val
return 0
if self._prev_fast_ma is None or self._prev_prev_fast_ma is None or self._prev_slow_ma is None:
self._prev_prev_fast_ma = self._prev_fast_ma
self._prev_fast_ma = fast_val
self._prev_slow_ma = slow_val
return 0
prev_f = self._prev_fast_ma
prev_f2 = self._prev_prev_fast_ma
prev_s = self._prev_slow_ma
signal = 0
if prev_f2 < prev_s and prev_f > prev_s:
signal = 1
elif prev_f2 > prev_s and prev_f < prev_s:
signal = -1
self._prev_prev_fast_ma = self._prev_fast_ma
self._prev_fast_ma = fast_val
self._prev_slow_ma = slow_val
return signal
def _update_rsi_signal(self, rsi_val):
if not self._rsi.IsFormed:
self._prev_prev_rsi = self._prev_rsi
self._prev_rsi = rsi_val
return 0
if self._prev_rsi is None or self._prev_prev_rsi is None:
self._prev_prev_rsi = self._prev_rsi
self._prev_rsi = rsi_val
return 0
prev = self._prev_rsi
prev2 = self._prev_prev_rsi
signal = 0
if prev2 < 30 and prev > 30:
signal = 1
elif prev2 > 70 and prev < 70:
signal = -1
self._prev_prev_rsi = self._prev_rsi
self._prev_rsi = rsi_val
return signal
def _update_cci_signal(self, cci_val):
if not self._cci.IsFormed:
self._prev_prev_cci = self._prev_cci
self._prev_cci = cci_val
return 0
if self._prev_cci is None or self._prev_prev_cci is None:
self._prev_prev_cci = self._prev_cci
self._prev_cci = cci_val
return 0
prev = self._prev_cci
prev2 = self._prev_prev_cci
signal = 0
if prev2 < -100 and prev > -100:
signal = 1
elif prev2 > 100 and prev < 100:
signal = -1
self._prev_prev_cci = self._prev_cci
self._prev_cci = cci_val
return signal
def _update_slope_signal(self, slope_val):
if not self._slope_ma.IsFormed:
self._prev_prev_slope_ma = self._prev_slope_ma
self._prev_slope_ma = slope_val
return 0
if self._prev_slope_ma is None or self._prev_prev_slope_ma is None:
self._prev_prev_slope_ma = self._prev_slope_ma
self._prev_slope_ma = slope_val
return 0
prev = self._prev_slope_ma
prev2 = self._prev_prev_slope_ma
signal = 0
if prev > prev2:
signal = 1
elif prev < prev2:
signal = -1
self._prev_prev_slope_ma = self._prev_slope_ma
self._prev_slope_ma = slope_val
return signal
def _update_ao_signal(self, ao_val):
if not self._ao.IsFormed:
self._prev_ao = ao_val
return 0
if self._prev_ao is None:
self._prev_ao = ao_val
return 0
prev = self._prev_ao
signal = 0
if ao_val > prev:
signal = 1
elif ao_val < prev:
signal = -1
self._prev_ao = ao_val
return signal
def _reset_after_exit(self):
self._entry_price = 0.0
self._stop_loss_price = 0.0
self._take_profit_price = 0.0
self._is_long_position = False
self._entry_candle_time = None
self._last_trade_direction = 0
self._has_last_signals = False
self._last_indicator_signals = [0] * 5
self._last_neuron_outputs = [0.0] * 5
def OnReseted(self):
super(perceptron_adaptive_strategy, self).OnReseted()
self._reset_state()
def CreateClone(self):
return perceptron_adaptive_strategy()