using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Ecng.Common;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Self-organizing-map style classifier trained on DeMark-derived candle feature vectors.
/// </summary>
public class RichKohonenMapStrategy : Strategy
{
private const int _vectorSize = 7;
private const int _buyCapacity = 10000;
private const int _sellCapacity = 10000;
private const int _holdCapacity = 25000;
private const int _fileSize = (_buyCapacity + _sellCapacity + _holdCapacity) * _vectorSize * sizeof(double);
private readonly StrategyParam<decimal> _minPips;
private readonly StrategyParam<decimal> _maxPips;
private readonly StrategyParam<decimal> _takeProfit;
private readonly StrategyParam<decimal> _stopLoss;
private readonly StrategyParam<decimal> _lots;
private readonly StrategyParam<int> _slippage;
private readonly StrategyParam<string> _mapPath;
private readonly StrategyParam<string> _eaName;
private readonly StrategyParam<DataType> _candleType;
private readonly List<ICandleMessage> _candles = [];
private readonly List<double[]> _buyMap = [];
private readonly List<double[]> _sellMap = [];
private readonly List<double[]> _holdMap = [];
public decimal MinPips { get => _minPips.Value; set => _minPips.Value = value; }
public decimal MaxPips { get => _maxPips.Value; set => _maxPips.Value = value; }
public decimal TakeProfit { get => _takeProfit.Value; set => _takeProfit.Value = value; }
public decimal StopLoss { get => _stopLoss.Value; set => _stopLoss.Value = value; }
public decimal Lots { get => _lots.Value; set => _lots.Value = value; }
public int Slippage { get => _slippage.Value; set => _slippage.Value = value; }
public string MapPath { get => _mapPath.Value; set => _mapPath.Value = value; }
public string EAName { get => _eaName.Value; set => _eaName.Value = value; }
public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
public RichKohonenMapStrategy()
{
// Pips are price steps. For the sample BTCUSDT (step 0.01) at 1h the band is 100-1000 USDT, the middle of the hourly
// open-to-open moves: the quietest quarter of the hours and the rare spikes train the hold map.
_minPips = Param(nameof(MinPips), 10000m).SetNotNegative().SetDisplay("Min Pips", "Minimum move classified as directional.", "Learning");
_maxPips = Param(nameof(MaxPips), 100000m).SetGreaterThanZero().SetDisplay("Max Pips", "Maximum move accepted as directional training sample.", "Learning");
_takeProfit = Param(nameof(TakeProfit), 0m).SetNotNegative().SetDisplay("Take Profit", "Compatibility parameter from the original EA.", "Trading");
_stopLoss = Param(nameof(StopLoss), 0m).SetNotNegative().SetDisplay("Stop Loss", "Compatibility parameter from the original EA.", "Trading");
_lots = Param(nameof(Lots), 0.1m).SetGreaterThanZero().SetDisplay("Lots", "Fallback volume when balance-based sizing returns zero.", "Trading");
_slippage = Param(nameof(Slippage), 3).SetNotNegative().SetDisplay("Slippage", "Compatibility slippage parameter.", "Trading");
_mapPath = Param(nameof(MapPath), "rl.bin").SetDisplay("Map Path", "Binary persistence path for Kohonen maps.", "Learning");
_eaName = Param(nameof(EAName), "Rich").SetDisplay("EA Name", "Informational EA name.", "General");
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame()).SetDisplay("Candle Type", "Feature extraction timeframe.", "General");
}
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
=> [(Security, CandleType)];
protected override void OnReseted()
{
base.OnReseted();
_candles.Clear();
_buyMap.Clear();
_sellMap.Clear();
_holdMap.Clear();
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
LoadMaps();
var subscription = SubscribeCandles(CandleType);
subscription.Bind(ProcessCandle).Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawOwnTrades(area);
}
}
protected override void OnStopped()
{
SaveMaps();
base.OnStopped();
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
_candles.Add(candle);
if (_candles.Count > 8)
_candles.RemoveAt(0);
if (_candles.Count < 7)
return;
var current = BuildVector(0);
var previous = BuildVector(1);
var decision = Classify(current);
SetTarget(decision);
var latest = _candles[^1];
var prior = _candles[^2];
var step = Security?.PriceStep ?? 1m;
if (step <= 0m)
step = 1m;
var movePips = (latest.OpenPrice - prior.OpenPrice) / step;
if (movePips >= MinPips && movePips <= MaxPips)
AddPrototype(_buyMap, previous, _buyCapacity);
else if (movePips <= -MinPips && movePips >= -MaxPips)
AddPrototype(_sellMap, previous, _sellCapacity);
else
AddPrototype(_holdMap, previous, _holdCapacity);
}
private int Classify(double[] vector)
{
var buyDistance = BestDistance(_buyMap, vector);
var sellDistance = BestDistance(_sellMap, vector);
var holdDistance = BestDistance(_holdMap, vector);
if (double.IsPositiveInfinity(buyDistance) &&
double.IsPositiveInfinity(sellDistance) &&
double.IsPositiveInfinity(holdDistance))
return 0;
if (buyDistance <= sellDistance && buyDistance <= holdDistance)
return 1;
if (sellDistance <= buyDistance && sellDistance <= holdDistance)
return -1;
return 0;
}
private void SetTarget(int decision)
{
var volume = CalculateVolume();
var target = decision * volume;
var difference = target - Position;
if (difference > 0m)
BuyMarket(difference);
else if (difference < 0m)
SellMarket(Math.Abs(difference));
}
private decimal CalculateVolume()
{
var balance = Portfolio?.CurrentValue ?? Portfolio?.BeginValue ?? 0m;
var volume = balance > 0m ? Math.Floor(balance / 50m) / 10m : 0m;
if (volume <= 0m)
volume = Lots;
if (Security?.MaxVolume is decimal max && max > 0m)
volume = Math.Min(volume, max);
if (Security?.MinVolume is decimal min && min > 0m)
volume = Math.Max(volume, min);
if (Security?.VolumeStep is decimal step && step > 0m)
volume = Math.Floor(volume / step) * step;
return volume > 0m ? volume : Lots;
}
private double[] BuildVector(int offset)
{
var lastIndex = _candles.Count - 1 - offset;
var firstIndex = lastIndex - 5;
var current = _candles[lastIndex];
double pivotSum = 0, r1Sum = 0, s1Sum = 0;
var high = decimal.MinValue;
var low = decimal.MaxValue;
for (var i = firstIndex; i < lastIndex; i++)
{
var c = _candles[i];
var (pivot, r1, s1) = DeMark(c.OpenPrice, c.HighPrice, c.LowPrice, c.ClosePrice);
pivotSum += (double)pivot;
r1Sum += (double)r1;
s1Sum += (double)s1;
high = Math.Max(high, c.HighPrice);
low = Math.Min(low, c.LowPrice);
}
// The five candles taken as one bar: first open, extreme high and low, last close.
var (barPivot, barR1, barS1) = DeMark(_candles[firstIndex].OpenPrice, high, low, _candles[lastIndex - 1].ClosePrice);
return
[
(double)current.OpenPrice,
pivotSum / 5d,
r1Sum / 5d,
s1Sum / 5d,
(double)barPivot,
(double)barR1,
(double)barS1,
];
}
private static (decimal pivot, decimal r1, decimal s1) DeMark(decimal open, decimal high, decimal low, decimal close)
{
var x = close < open
? high + 2m * low + close
: close > open
? 2m * high + low + close
: high + low + 2m * close;
return (x / 4m, x / 2m - low, x / 2m - high);
}
private static double BestDistance(List<double[]> map, double[] vector)
{
var best = double.PositiveInfinity;
foreach (var prototype in map)
{
var sum = 0d;
for (var i = 0; i < _vectorSize; i++)
{
var d = vector[i] - prototype[i];
sum += d * d;
}
best = Math.Min(best, Math.Sqrt(sum));
}
return best;
}
private static void AddPrototype(List<double[]> map, double[] vector, int capacity)
{
if (map.Count < capacity)
map.Add((double[])vector.Clone());
}
private void LoadMaps()
{
if (MapPath.IsEmpty() || !File.Exists(MapPath))
return;
try
{
using var reader = new BinaryReader(File.OpenRead(MapPath));
if (reader.BaseStream.Length < _fileSize)
throw new InvalidDataException($"the file holds {reader.BaseStream.Length} bytes, the three matrices need {_fileSize}");
ReadMap(reader, _buyMap, _buyCapacity);
ReadMap(reader, _sellMap, _sellCapacity);
ReadMap(reader, _holdMap, _holdCapacity);
}
catch (Exception ex)
{
_buyMap.Clear();
_sellMap.Clear();
_holdMap.Clear();
LogWarning($"Unable to load Kohonen map '{MapPath}': {ex.Message}");
}
}
private void SaveMaps()
{
if (MapPath.IsEmpty())
return;
try
{
using var writer = new BinaryWriter(File.Create(MapPath));
WriteMap(writer, _buyMap, _buyCapacity);
WriteMap(writer, _sellMap, _sellCapacity);
WriteMap(writer, _holdMap, _holdCapacity);
}
catch (Exception ex)
{
LogWarning($"Unable to save Kohonen map '{MapPath}': {ex.Message}");
}
}
/// <summary>
/// Reads a zero-padded matrix of <paramref name="capacity"/> rows and keeps its non-empty rows.
/// </summary>
private static void ReadMap(BinaryReader reader, List<double[]> map, int capacity)
{
map.Clear();
for (var i = 0; i < capacity; i++)
{
var vector = new double[_vectorSize];
for (var j = 0; j < _vectorSize; j++)
vector[j] = reader.ReadDouble();
if (vector.Any(value => value != 0d))
map.Add(vector);
}
}
/// <summary>
/// Writes the map as a matrix of <paramref name="capacity"/> rows, padding the unused rows with zeros.
/// </summary>
private static void WriteMap(BinaryWriter writer, List<double[]> map, int capacity)
{
for (var i = 0; i < capacity; i++)
{
for (var j = 0; j < _vectorSize; j++)
writer.Write(i < map.Count ? map[i][j] : 0d);
}
}
}
import clr
import os
import struct
import math
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Math, Decimal
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Strategies import Strategy
class rich_kohonen_map_strategy(Strategy):
VECTOR_SIZE = 7
BUY_CAPACITY = 10000
SELL_CAPACITY = 10000
HOLD_CAPACITY = 25000
FILE_SIZE = (BUY_CAPACITY + SELL_CAPACITY + HOLD_CAPACITY) * VECTOR_SIZE * 8
def __init__(self):
super(rich_kohonen_map_strategy, self).__init__()
# Pips are price steps. For the sample BTCUSDT (step 0.01) at 1h the band is 100-1000 USDT, the middle of the hourly
# open-to-open moves: the quietest quarter of the hours and the rare spikes train the hold map.
self._min_pips = self.Param("MinPips", 10000.0).SetNotNegative()
self._max_pips = self.Param("MaxPips", 100000.0).SetGreaterThanZero()
self._take_profit = self.Param("TakeProfit", 0.0).SetNotNegative()
self._stop_loss = self.Param("StopLoss", 0.0).SetNotNegative()
self._lots = self.Param("Lots", 0.1).SetGreaterThanZero()
self._slippage = self.Param("Slippage", 3).SetNotNegative()
self._map_path = self.Param("MapPath", "rl.bin")
self._ea_name = self.Param("EAName", "Rich")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(1)))
self._candles = []
self._buy_map = []
self._sell_map = []
self._hold_map = []
@property
def CandleType(self):
return self._candle_type.Value
def GetWorkingSecurities(self):
return [(self.Security, self.CandleType)]
def OnReseted(self):
super(rich_kohonen_map_strategy, self).OnReseted()
self._candles = []
self._buy_map = []
self._sell_map = []
self._hold_map = []
def OnStarted2(self, time):
super(rich_kohonen_map_strategy, self).OnStarted2(time)
self._load_maps()
self.SubscribeCandles(self.CandleType).Bind(self._process_candle).Start()
def OnStopped(self):
self._save_maps()
super(rich_kohonen_map_strategy, self).OnStopped()
def _process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
self._candles.append(candle)
if len(self._candles) > 8:
del self._candles[0]
if len(self._candles) < 7:
return
current = self._build_vector(0)
previous = self._build_vector(1)
decision = self._classify(current)
self._set_target(decision)
latest = self._candles[-1]
prior = self._candles[-2]
step = self.Security.PriceStep if self.Security is not None and self.Security.PriceStep is not None else Decimal.One
if step <= 0:
step = Decimal.One
# Exact decimal steps: in binary floating point a move of exactly MinPips or MaxPips can fall outside the band.
move_pips = (latest.OpenPrice - prior.OpenPrice) / step
min_pips = Decimal(self._min_pips.Value)
max_pips = Decimal(self._max_pips.Value)
if min_pips <= move_pips <= max_pips:
self._add_prototype(self._buy_map, previous, self.BUY_CAPACITY)
elif -max_pips <= move_pips <= -min_pips:
self._add_prototype(self._sell_map, previous, self.SELL_CAPACITY)
else:
self._add_prototype(self._hold_map, previous, self.HOLD_CAPACITY)
def _classify(self, vector):
distances = [
(self._best_distance(self._buy_map, vector), 1),
(self._best_distance(self._sell_map, vector), -1),
(self._best_distance(self._hold_map, vector), 0),
]
finite = [item for item in distances if not math.isinf(item[0])]
if not finite:
return 0
return min(finite, key=lambda item: item[0])[1]
def _set_target(self, decision):
volume = self._calculate_volume()
target = decision * volume
difference = target - float(self.Position)
if difference > 0:
self.BuyMarket(difference)
elif difference < 0:
self.SellMarket(abs(difference))
def _calculate_volume(self):
balance = 0.0
if self.Portfolio is not None:
if self.Portfolio.CurrentValue is not None:
balance = float(self.Portfolio.CurrentValue)
elif self.Portfolio.BeginValue is not None:
balance = float(self.Portfolio.BeginValue)
volume = math.floor(balance / 50.0) / 10.0 if balance > 0 else 0.0
if volume <= 0:
volume = float(self._lots.Value)
if self.Security is not None:
if self.Security.MaxVolume is not None and float(self.Security.MaxVolume) > 0:
volume = min(volume, float(self.Security.MaxVolume))
if self.Security.MinVolume is not None and float(self.Security.MinVolume) > 0:
volume = max(volume, float(self.Security.MinVolume))
if self.Security.VolumeStep is not None and float(self.Security.VolumeStep) > 0:
step = float(self.Security.VolumeStep)
volume = math.floor(volume / step) * step
return volume if volume > 0 else float(self._lots.Value)
def _build_vector(self, offset):
last_index = len(self._candles) - 1 - offset
first_index = last_index - 5
current = self._candles[last_index]
window = self._candles[first_index:last_index]
pivot_sum = r1_sum = s1_sum = 0.0
for c in window:
pivot, r1, s1 = self._demark(float(c.OpenPrice), float(c.HighPrice), float(c.LowPrice), float(c.ClosePrice))
pivot_sum += pivot
r1_sum += r1
s1_sum += s1
# The five candles taken as one bar: first open, extreme high and low, last close.
bar_pivot, bar_r1, bar_s1 = self._demark(
float(window[0].OpenPrice),
max(float(c.HighPrice) for c in window),
min(float(c.LowPrice) for c in window),
float(window[-1].ClosePrice))
return [
float(current.OpenPrice),
pivot_sum / 5.0,
r1_sum / 5.0,
s1_sum / 5.0,
bar_pivot,
bar_r1,
bar_s1,
]
@staticmethod
def _demark(o, h, l, close):
if close < o:
x = h + 2.0 * l + close
elif close > o:
x = 2.0 * h + l + close
else:
x = h + l + 2.0 * close
return x / 4.0, x / 2.0 - l, x / 2.0 - h
@staticmethod
def _best_distance(map_values, vector):
if not map_values:
return float("inf")
return min(math.sqrt(sum((vector[i] - prototype[i]) ** 2 for i in range(7))) for prototype in map_values)
def _add_prototype(self, map_values, vector, capacity):
if len(map_values) < capacity:
map_values.append(list(vector))
def _load_maps(self):
path = str(self._map_path.Value)
if not path or not os.path.isfile(path):
return
try:
with open(path, "rb") as f:
data = f.read(self.FILE_SIZE)
if len(data) < self.FILE_SIZE:
raise IOError("the file holds {0} bytes, the three matrices need {1}".format(len(data), self.FILE_SIZE))
buy_map, offset = self._read_map(data, 0, self.BUY_CAPACITY)
sell_map, offset = self._read_map(data, offset, self.SELL_CAPACITY)
hold_map, offset = self._read_map(data, offset, self.HOLD_CAPACITY)
self._buy_map = buy_map
self._sell_map = sell_map
self._hold_map = hold_map
except Exception as ex:
self._buy_map = []
self._sell_map = []
self._hold_map = []
self.LogWarning("Unable to load Kohonen map '{0}': {1}".format(path, ex))
def _save_maps(self):
path = str(self._map_path.Value)
if not path:
return
try:
with open(path, "wb") as f:
self._write_map(f, self._buy_map, self.BUY_CAPACITY)
self._write_map(f, self._sell_map, self.SELL_CAPACITY)
self._write_map(f, self._hold_map, self.HOLD_CAPACITY)
except Exception as ex:
self.LogWarning("Unable to save Kohonen map '{0}': {1}".format(path, ex))
@classmethod
def _read_map(cls, data, offset, capacity):
# A zero-padded matrix of capacity rows; only its non-empty rows are kept.
count = capacity * cls.VECTOR_SIZE
end = offset + count * 8
values = struct.unpack("<%dd" % count, data[offset:end])
empty = (0.0,) * cls.VECTOR_SIZE
rows = []
for i in range(0, count, cls.VECTOR_SIZE):
row = values[i:i + cls.VECTOR_SIZE]
if row != empty:
rows.append(list(row))
return rows, end
@classmethod
def _write_map(cls, f, values, capacity):
# A matrix of capacity rows whose unused rows are padded with zeros.
row_format = "<%dd" % cls.VECTOR_SIZE
for vector in values:
f.write(struct.pack(row_format, *vector))
f.write(bytes(8 * cls.VECTOR_SIZE * (capacity - len(values))))
def CreateClone(self):
return rich_kohonen_map_strategy()