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>
/// Strategy that analyzes historical candle bodies for the same time of day.
/// Opens a position when bullish or bearish pressure dominates over recent days.
/// Implements simple martingale sizing after losing trades.
/// </summary>
public class StatisticsRepeatingBehaviorStrategy : Strategy
{
private readonly StrategyParam<int> _historyDays;
private readonly StrategyParam<int> _minimumBodyPoints;
private readonly StrategyParam<int> _stopLossPips;
private readonly StrategyParam<decimal> _initialVolume;
private readonly StrategyParam<decimal> _martingaleFactor;
private readonly StrategyParam<DataType> _candleType;
private readonly System.Collections.Concurrent.ConcurrentDictionary<int, BodyStatistics> _bodyStatistics = new();
private decimal _currentVolume;
private decimal _entryPrice;
private decimal _stopPrice;
private int _positionDirection;
private decimal _priceStep;
private TimeSpan _timeFrame;
/// <summary>
/// Number of historical days to aggregate for statistics.
/// </summary>
public int HistoryDays
{
get => _historyDays.Value;
set => _historyDays.Value = value;
}
/// <summary>
/// Minimum body size in points for a candle to contribute into the statistics.
/// </summary>
public int MinimumBodyPoints
{
get => _minimumBodyPoints.Value;
set => _minimumBodyPoints.Value = value;
}
/// <summary>
/// Stop loss distance measured in pips.
/// </summary>
public int StopLossPips
{
get => _stopLossPips.Value;
set => _stopLossPips.Value = value;
}
/// <summary>
/// Initial order size used before applying martingale adjustments.
/// </summary>
public decimal InitialVolume
{
get => _initialVolume.Value;
set => _initialVolume.Value = value;
}
/// <summary>
/// Multiplier applied to the order size after a losing trade.
/// </summary>
public decimal MartingaleFactor
{
get => _martingaleFactor.Value;
set => _martingaleFactor.Value = value;
}
/// <summary>
/// Candle type to analyze.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initializes a new instance of <see cref="StatisticsRepeatingBehaviorStrategy"/>.
/// </summary>
public StatisticsRepeatingBehaviorStrategy()
{
_historyDays = Param(nameof(HistoryDays), 3)
.SetGreaterThanZero()
.SetDisplay("History Days", "Number of days to collect statistics", "Parameters")
;
_minimumBodyPoints = Param(nameof(MinimumBodyPoints), 0)
.SetDisplay("Minimum Body (points)", "Ignore candles with smaller body", "Parameters")
;
_stopLossPips = Param(nameof(StopLossPips), 15)
.SetGreaterThanZero()
.SetDisplay("Stop Loss (pips)", "Stop loss distance in pips", "Risk")
;
_initialVolume = Param(nameof(InitialVolume), 1m)
.SetGreaterThanZero()
.SetDisplay("Initial Volume", "Starting order size", "Trading")
;
_martingaleFactor = Param(nameof(MartingaleFactor), 1.618m)
.SetGreaterThanZero()
.SetDisplay("Martingale Factor", "Multiplier after losing trade", "Trading")
;
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
.SetDisplay("Candle Type", "Candles for analysis", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_bodyStatistics.Clear();
_currentVolume = 0m;
_entryPrice = 0m;
_stopPrice = 0m;
_positionDirection = 0;
_priceStep = 0m;
_timeFrame = TimeSpan.Zero;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_priceStep = Security.PriceStep ?? 1m;
_timeFrame = CandleType.Arg is TimeSpan span ? span : TimeSpan.Zero;
if (_timeFrame <= TimeSpan.Zero)
_timeFrame = TimeSpan.FromMinutes(1);
_currentVolume = AdjustVolume(InitialVolume);
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
var nextOpen = candle.OpenTime + _timeFrame;
var nextKey = GetMinuteKey(nextOpen);
// Close existing position at the beginning of the new bar.
if (_positionDirection != 0)
{
var exitPrice = candle.ClosePrice;
var stopHit = false;
if (_positionDirection > 0)
{
if (candle.LowPrice <= _stopPrice)
{
exitPrice = _stopPrice;
stopHit = true;
}
}
else
{
if (candle.HighPrice >= _stopPrice)
{
exitPrice = _stopPrice;
stopHit = true;
}
}
if (Position > 0)
SellMarket();
else if (Position < 0)
BuyMarket();
UpdateVolumeAfterTrade(exitPrice, stopHit);
}
if (_positionDirection == 0 && _bodyStatistics.TryGetValue(nextKey, out var stats) && stats.Count > 0)
{
var bullSum = stats.BullSum;
var bearSum = stats.BearSum;
if (bullSum > bearSum && Position <= 0)
{
EnterPosition(candle, true);
}
else if (bearSum > bullSum && Position >= 0)
{
EnterPosition(candle, false);
}
}
UpdateStatistics(candle);
}
private void EnterPosition(ICandleMessage candle, bool isLong)
{
var volume = _currentVolume;
if (volume <= 0m)
return;
var stopDistance = StopLossPips * _priceStep;
if (stopDistance <= 0m)
stopDistance = _priceStep;
if (isLong)
{
BuyMarket();
_entryPrice = candle.ClosePrice;
_stopPrice = _entryPrice - stopDistance;
_positionDirection = 1;
}
else
{
SellMarket();
_entryPrice = candle.ClosePrice;
_stopPrice = _entryPrice + stopDistance;
_positionDirection = -1;
}
}
private void UpdateVolumeAfterTrade(decimal exitPrice, bool stopHit)
{
if (_positionDirection == 0)
return;
var profit = (_positionDirection > 0 ? exitPrice - _entryPrice : _entryPrice - exitPrice);
if (profit > 0m && !stopHit)
{
_currentVolume = AdjustVolume(InitialVolume);
}
else
{
var increased = AdjustVolume(InitialVolume * MartingaleFactor);
_currentVolume = increased;
}
_entryPrice = 0m;
_stopPrice = 0m;
_positionDirection = 0;
}
private void UpdateStatistics(ICandleMessage candle)
{
var currentKey = GetMinuteKey(candle.OpenTime);
var stats = _bodyStatistics.GetOrAdd(currentKey, _ => new BodyStatistics());
var body = candle.ClosePrice - candle.OpenPrice;
var bodyPoints = body / _priceStep;
var absBody = Math.Abs(bodyPoints);
if (MinimumBodyPoints > 0 && absBody < MinimumBodyPoints)
return;
stats.Enqueue(bodyPoints);
while (stats.Count > HistoryDays)
{
var removed = stats.Dequeue();
if (removed > 0m)
stats.BullSum -= removed;
else if (removed < 0m)
stats.BearSum -= Math.Abs(removed);
}
}
private decimal AdjustVolume(decimal volume)
{
return volume <= 0m ? 1m : volume;
}
private static int GetMinuteKey(DateTimeOffset time)
{
return time.Hour * 60 + time.Minute;
}
private sealed class BodyStatistics
{
private readonly Queue<decimal> _values = new();
public decimal BullSum { get; set; }
public decimal BearSum { get; set; }
public int Count => _values.Count;
public void Enqueue(decimal value)
{
_values.Enqueue(value);
if (value > 0m)
BullSum += value;
else if (value < 0m)
BearSum += Math.Abs(value);
}
public decimal Dequeue()
{
return _values.Dequeue();
}
}
}
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 datatype_extensions import *
from indicator_extensions import *
from collections import deque
class statistics_repeating_behavior_strategy(Strategy):
"""Candle body statistics by time-of-day with martingale sizing."""
def __init__(self):
super(statistics_repeating_behavior_strategy, self).__init__()
self._history_days = self.Param("HistoryDays", 3).SetGreaterThanZero().SetDisplay("History Days", "Number of days to collect statistics", "Parameters")
self._minimum_body_points = self.Param("MinimumBodyPoints", 0).SetDisplay("Minimum Body (points)", "Ignore candles with smaller body", "Parameters")
self._stop_loss_pips = self.Param("StopLossPips", 15).SetGreaterThanZero().SetDisplay("Stop Loss (pips)", "Stop loss distance in pips", "Risk")
self._martingale_factor = self.Param("MartingaleFactor", 1.618).SetGreaterThanZero().SetDisplay("Martingale Factor", "Multiplier after losing trade", "Trading")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4))).SetDisplay("Candle Type", "Candles for analysis", "General")
@property
def CandleType(self): return self._candle_type.Value
@CandleType.setter
def CandleType(self, value): self._candle_type.Value = value
def OnReseted(self):
super(statistics_repeating_behavior_strategy, self).OnReseted()
self._body_stats = {}
self._entry_price = 0
self._stop_price = 0
self._pos_dir = 0
self._timeframe_minutes = 0
def OnStarted2(self, time):
super(statistics_repeating_behavior_strategy, self).OnStarted2(time)
self._body_stats = {}
self._entry_price = 0
self._stop_price = 0
self._pos_dir = 0
ct = self.CandleType
arg = ct.Arg
if hasattr(arg, 'TotalMinutes'):
self._timeframe_minutes = int(arg.TotalMinutes)
else:
self._timeframe_minutes = 1
sub = self.SubscribeCandles(self.CandleType)
sub.Bind(self.OnProcess).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, sub)
self.DrawOwnTrades(area)
def OnProcess(self, candle):
if candle.State != CandleStates.Finished:
return
open_time = candle.OpenTime
next_key = self._get_minute_key_offset(open_time, self._timeframe_minutes)
close = float(candle.ClosePrice)
high = float(candle.HighPrice)
low = float(candle.LowPrice)
open_p = float(candle.OpenPrice)
stop_pips = self._stop_loss_pips.Value
# Close existing position
if self._pos_dir != 0:
exit_price = close
stop_hit = False
if self._pos_dir > 0:
if low <= self._stop_price:
exit_price = self._stop_price
stop_hit = True
else:
if high >= self._stop_price:
exit_price = self._stop_price
stop_hit = True
if self.Position > 0:
self.SellMarket()
elif self.Position < 0:
self.BuyMarket()
profit = (exit_price - self._entry_price) if self._pos_dir > 0 else (self._entry_price - exit_price)
self._entry_price = 0
self._stop_price = 0
self._pos_dir = 0
# Entry based on statistics
if self._pos_dir == 0 and next_key in self._body_stats:
stats = self._body_stats[next_key]
if len(stats['values']) > 0:
bull_sum = stats['bull_sum']
bear_sum = stats['bear_sum']
if bull_sum > bear_sum and self.Position <= 0:
self._entry_price = close
self._stop_price = close - stop_pips
self._pos_dir = 1
self.BuyMarket()
elif bear_sum > bull_sum and self.Position >= 0:
self._entry_price = close
self._stop_price = close + stop_pips
self._pos_dir = -1
self.SellMarket()
# Update statistics
current_key = open_time.Hour * 60 + open_time.Minute
if current_key not in self._body_stats:
self._body_stats[current_key] = {'values': deque(), 'bull_sum': 0, 'bear_sum': 0}
stats = self._body_stats[current_key]
body = close - open_p
min_body = self._minimum_body_points.Value
abs_body = abs(body)
if min_body > 0 and abs_body < min_body:
return
stats['values'].append(body)
if body > 0:
stats['bull_sum'] += body
elif body < 0:
stats['bear_sum'] += abs(body)
while len(stats['values']) > self._history_days.Value:
removed = stats['values'].popleft()
if removed > 0:
stats['bull_sum'] -= removed
elif removed < 0:
stats['bear_sum'] -= abs(removed)
def _get_minute_key_offset(self, dt, offset_minutes):
total = dt.Hour * 60 + dt.Minute + offset_minutes
return total % 1440
def CreateClone(self):
return statistics_repeating_behavior_strategy()