Perceptron 自适应策略
概述
本策略将 MetaTrader 5 专家顾问 Perceptron.mq5 移植到 StockSharp。
五个离散指标信号通过两层感知机组合,仓位平仓后根据盈亏对突触权重进行奖励或惩罚,从而复制原始 EA 的自学习机制,同时使用 StockSharp 的高级蜡烛 API。
指标层
| 代码 | 描述 | 信号规则 |
|---|---|---|
IND1 |
快速/慢速 SMA 金叉死叉 | 在上一根柱子上发生金叉时返回 +1,死叉时返回 −1,否则返回 0。 |
IND2 |
RSI | RSI 从超卖区间向上穿越 30 时返回 +1,从超买区间向下穿越 70 时返回 −1。 |
IND3 |
CCI | 向上突破 −100 时返回 +1,向下跌破 +100 时返回 −1。 |
IND4 |
短期 SMA 斜率 | 短期 SMA 在最近两根柱子之间上升时返回 +1,下降时返回 −1。 |
IND5 |
Awesome Oscillator 柱体颜色 | 当前柱体高于上一柱体(多头颜色)返回 +1,低于上一柱体(空头颜色)返回 −1。 |
所有信号均在收盘蜡烛上计算,内部维护的历史缓存模拟 MQL5 中的 CopyBuffer 行为。
感知机结构
- 五个隐藏神经元 (
NN1…NN5) 分别组合四个指标,连线方式与原策略保持一致。 - 每个神经元拥有一组突触权重以及偏置权重 (
NNS1…NNS5)。 - 最终输出
brainReturn为所有神经元输出与偏置的加权和。brainReturn > 0且上一笔不是多头时尝试开多。brainReturn < 0且上一笔不是空头时尝试开空。
- 仅在没有持仓时发送市价单。
仓位管理
- 入场时记录价格、方向以及各指标/神经元的状态。
- 止损和止盈以绝对价格单位设置(例如 0.0004 对应外汇五位小数的 4 点)。
下一根蜡烛开盘后:- 多头首先比较最高价是否达到止盈,然后比较最低价是否触发止损;
- 空头首先比较最低价是否达到止盈,然后比较最高价是否触发止损;
- 若同一根蜡烛同时触及两个水平,则优先判定止盈,与原 EA 的偏好一致。
- 检测到离场后使用市价单平仓,并根据触发的止盈/止损价格计算实际盈亏。
权重自适应
平仓后使用记录的信号重新评估:
- 计算
directionSign(多头为 −1,空头为 +1)以及outcomeSign(实际盈亏的符号)。 - 偏置权重限制在
[SinMin, SinMax]:若sign(neuronOutput) * directionSign > 0,在盈利时增加、亏损时减少,否则反向调整。 - 指标突触权重采用同样的逻辑,但不设上下界。
- 清除已存储的信号,避免下一笔交易重复使用。
该流程将原脚本中大量的条件语句浓缩为简洁的强化学习步骤。
参数
| 参数 | 默认值 | 说明 |
|---|---|---|
CandleType |
1 分钟 | 策略使用的蜡烛类型。 |
FastMaLength |
5 | 快速 SMA 周期。 |
SlowMaLength |
9 | 慢速 SMA 周期。 |
RsiLength |
14 | RSI 周期。 |
CciLength |
14 | CCI 周期。 |
SlopeMaLength |
5 | 斜率 SMA 周期。 |
AoShortLength |
5 | Awesome Oscillator 短周期。 |
AoLongLength |
34 | Awesome Oscillator 长周期。 |
StopLossOffset |
0.001 | 止损距离,绝对价格单位(0 表示禁用)。 |
TakeProfitOffset |
0.0004 | 止盈距离,绝对价格单位(0 表示禁用)。 |
SinMax |
5 | 神经元偏置最大值。 |
SinMin |
0 | 神经元偏置最小值。 |
SinPlusStep |
0.03 | 正向强化步长。 |
SinMinusStep |
0.03 | 负向强化步长。 |
所有参数均通过 StrategyParam<T> 暴露,可在 Designer 中优化。
实现说明
- 使用高级蜡烛订阅接口,一次绑定多个指标。
- 为了在更新权重时精确掌握平仓价格,策略内部自行处理止盈止损。
- 通过
nullable字段存储历史数值,确保指标在完全形成之前不会触发信号。 - Awesome Oscillator 的颜色通过比较当前与上一柱体的数值近似获得。
- 图表区域绘制蜡烛、快慢均线及策略成交记录,便于观察学习过程。
限制与假设
- 只有在蜡烛收盘后才会检查止盈/止损,无法得知同一根蜡烛内的触发顺序,因此优先选择止盈结果。
- 指标权重没有上下界,长时间强化可能导致数值偏大。
- 原 EA 中
LastTradeType不会重置;在本移植中每次平仓后都会清零,以允许连续同向交易。
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()