Wss Trader
Порт эксперта MetaTrader 4 «Wss_trader» с сайта forex-instruments.info. Первоначальная версия использует уровни Камариллы и классические пивоты, открывая не более одной сделки за свечу, когда цена пробивает расчётные границы в рабочее время.
Логика стратегии
- В начале каждого нового торгового дня считываются максимум, минимум и закрытие предыдущих суток для построения лестницы уровней:
Pivot = (High + Low + Close) / 3Long entry = Pivot + Metric × pointShort entry = Pivot − Metric × pointLong stop = Short entryShort stop = Long entry- Цели повторяют формулы MetaTrader
Close ± (High − Low) × 1.1 / 2с тем же ограничителем, что и в оригинальном коде.
- Торговля разрешена только между
Start HourиEnd Hour(включительно). Вне окна открытая позиция немедленно закрывается. - Если закрытие свечи пересекает длинный уровень (текущее закрытие ≥ уровень и предыдущее закрытие < уровень), стратегия покупает один раз указанным объёмом, фиксирует рассчитанные стоп и тейк, а также блокирует повторные входы на текущей свече. Для коротких сигналов действует зеркальное правило.
- При положительном движении не менее
Trailing Pointsшагов цены стоп подтягивается, сохраняя расстояние до закрытия, и никогда не отходит назад.
Параметры
| Имя | Описание | Значение по умолчанию |
|---|---|---|
Working Candle |
Тип свечей для внутридневных расчётов. | 15 Minute |
Daily Candle |
Тип свечей для чтения предыдущего дня. | 1 Day |
Start Hour |
Час (0-23), с которого разрешена торговля. | 8 |
End Hour |
Час (0-23), после которого новые сделки запрещены. | 16 |
Metric Points |
Расстояние от пивота до уровней пробоя в шагах цены. | 20 |
Trailing Points |
Дистанция трейлинг-стопа в шагах цены (0 отключает трейлинг). |
20 |
Order Volume |
Объём заявки, соответствующий параметру lots из MQL. |
0.1 |
Примечания
- Позиция закрывается сразу после завершения торгового окна, как и в оригинальном советнике.
- Трейлинг рассчитывается по закрытым свечам: внутрибарая подтяжка не воспроизводится, поскольку порт работает на данных закрытия.
- На каждой свече допускается только один вход, что соответствует флагу
tenbв исходнике.
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>
/// Port of the "Wss_trader" MetaTrader strategy built around Camarilla and classic pivot levels.
/// Reproduces the time-filtered breakout entries, fixed targets, and optional trailing stop.
/// </summary>
public class WssTraderStrategy : Strategy
{
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<DataType> _dailyCandleType;
private readonly StrategyParam<int> _startHour;
private readonly StrategyParam<int> _endHour;
private readonly StrategyParam<int> _metricPoints;
private readonly StrategyParam<int> _trailingPoints;
private readonly StrategyParam<decimal> _orderVolume;
private ICandleMessage _previousDailyCandle;
private decimal _priceStep;
private decimal _longEntryLevel;
private decimal _shortEntryLevel;
private decimal _longStopLevel;
private decimal _shortStopLevel;
private decimal _longTargetLevel;
private decimal _shortTargetLevel;
private decimal _previousClose;
private bool _hasPreviousClose;
private bool _levelsReady;
private bool _canTrade;
private DateTimeOffset? _lastCandleOpenTime;
private decimal _longEntryPrice;
private decimal _shortEntryPrice;
private decimal _longStop;
private decimal _shortStop;
private decimal _longTarget;
private decimal _shortTarget;
/// <summary>
/// Initializes a new instance of the <see cref="WssTraderStrategy"/> class.
/// </summary>
public WssTraderStrategy()
{
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
.SetDisplay("Working Candle", "Primary candle type for trading logic.", "General");
_dailyCandleType = Param(nameof(DailyCandleType), TimeSpan.FromHours(4).TimeFrame())
.SetDisplay("Daily Candle", "Daily candle type used for pivot calculation.", "General");
_startHour = Param(nameof(StartHour), 0)
.SetDisplay("Start Hour", "Hour of day when trading becomes active (0-23).", "Session");
_endHour = Param(nameof(EndHour), 23)
.SetDisplay("End Hour", "Hour of day after which trading is disabled (0-23).", "Session");
_metricPoints = Param(nameof(MetricPoints), 20)
.SetGreaterThanZero()
.SetDisplay("Metric Points", "Distance from the pivot to entry levels expressed in price steps.", "Levels");
_trailingPoints = Param(nameof(TrailingPoints), 20)
.SetDisplay("Trailing Points", "Trailing stop offset in price steps (0 disables trailing).", "Risk");
_orderVolume = Param(nameof(OrderVolume), 0.1m)
.SetGreaterThanZero()
.SetDisplay("Order Volume", "Order volume replicated from the original lots parameter.", "Orders");
}
/// <summary>
/// Candle type used for the trading calculations.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Daily candle type responsible for pivot extraction.
/// </summary>
public DataType DailyCandleType
{
get => _dailyCandleType.Value;
set => _dailyCandleType.Value = value;
}
/// <summary>
/// Hour of day when the strategy begins accepting signals.
/// </summary>
public int StartHour
{
get => _startHour.Value;
set => _startHour.Value = value;
}
/// <summary>
/// Hour of day when the strategy stops opening trades.
/// </summary>
public int EndHour
{
get => _endHour.Value;
set => _endHour.Value = value;
}
/// <summary>
/// Distance from the pivot to breakout levels in price steps.
/// </summary>
public int MetricPoints
{
get => _metricPoints.Value;
set => _metricPoints.Value = value;
}
/// <summary>
/// Trailing stop distance in price steps.
/// </summary>
public int TrailingPoints
{
get => _trailingPoints.Value;
set => _trailingPoints.Value = value;
}
/// <summary>
/// Order volume used for entries.
/// </summary>
public decimal OrderVolume
{
get => _orderVolume.Value;
set => _orderVolume.Value = value;
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
if (Security == null)
yield break;
yield return (Security, CandleType);
if (DailyCandleType != null && !Equals(DailyCandleType, CandleType))
yield return (Security, DailyCandleType);
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_previousDailyCandle = null;
_priceStep = 0m;
_longEntryLevel = 0m;
_shortEntryLevel = 0m;
_longStopLevel = 0m;
_shortStopLevel = 0m;
_longTargetLevel = 0m;
_shortTargetLevel = 0m;
_previousClose = 0m;
_hasPreviousClose = false;
_levelsReady = false;
_canTrade = true;
_lastCandleOpenTime = null;
_longEntryPrice = 0m;
_shortEntryPrice = 0m;
_longStop = 0m;
_shortStop = 0m;
_longTarget = 0m;
_shortTarget = 0m;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_priceStep = Security?.PriceStep ?? 0m;
if (_priceStep <= 0m)
_priceStep = 0.0001m;
var subscription = SubscribeCandles(CandleType);
subscription.Bind(ProcessCandle).Start();
var dailySubscription = SubscribeCandles(DailyCandleType);
dailySubscription.Bind(ProcessDailyCandle).Start();
StartProtection(null, null);
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawOwnTrades(area);
}
}
private void ProcessDailyCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
if (_previousDailyCandle != null)
{
CalculatePivotLevels(_previousDailyCandle);
_levelsReady = true;
}
_previousDailyCandle = candle;
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
if (_lastCandleOpenTime != candle.OpenTime)
{
_canTrade = true;
_lastCandleOpenTime = candle.OpenTime;
}
var withinHours = IsWithinTradingHours(candle.CloseTime);
if (!withinHours)
{
if (Position != 0m)
{
if (Position > 0) SellMarket(); else BuyMarket();
ResetPositionState();
}
_previousClose = candle.ClosePrice;
_hasPreviousClose = true;
return;
}
ManagePositions(candle);
if (!_levelsReady)
{
_previousClose = candle.ClosePrice;
_hasPreviousClose = true;
return;
}
if (!_hasPreviousClose)
{
_previousClose = candle.ClosePrice;
_hasPreviousClose = true;
return;
}
if (!_canTrade || Position != 0m)
{
_previousClose = candle.ClosePrice;
return;
}
var close = candle.ClosePrice;
var volume = AdjustVolume(OrderVolume);
if (volume > 0m && _previousClose < _longEntryLevel && close >= _longEntryLevel)
{
BuyMarket();
_canTrade = false;
_longEntryPrice = close;
_longStop = RoundPrice(_longStopLevel);
_longTarget = RoundPrice(_longTargetLevel);
_previousClose = close;
return;
}
if (volume > 0m && _previousClose > _shortEntryLevel && close <= _shortEntryLevel)
{
SellMarket();
_canTrade = false;
_shortEntryPrice = close;
_shortStop = RoundPrice(_shortStopLevel);
_shortTarget = RoundPrice(_shortTargetLevel);
_previousClose = close;
return;
}
_previousClose = close;
}
private void ManagePositions(ICandleMessage candle)
{
if (Position > 0m)
{
ManageLongPosition(candle);
}
else if (Position < 0m)
{
ManageShortPosition(candle);
}
}
private void ManageLongPosition(ICandleMessage candle)
{
var stop = _longStop;
var target = _longTarget;
var volume = Position;
if (volume <= 0m)
return;
if (stop > 0m && candle.LowPrice <= stop)
{
SellMarket();
ResetLongState();
return;
}
if (target > 0m && candle.HighPrice >= target)
{
SellMarket();
ResetLongState();
return;
}
var trailingDistance = ConvertPointsToPrice(TrailingPoints);
if (trailingDistance <= 0m || _longEntryPrice <= 0m)
return;
if (candle.ClosePrice - _longEntryPrice >= trailingDistance)
{
var newStop = RoundPrice(candle.ClosePrice - trailingDistance);
if (newStop > _longStop)
{
_longStop = newStop;
if (candle.LowPrice <= _longStop)
{
SellMarket();
ResetLongState();
}
}
}
}
private void ManageShortPosition(ICandleMessage candle)
{
var stop = _shortStop;
var target = _shortTarget;
var volume = Math.Abs(Position);
if (volume <= 0m)
return;
if (stop > 0m && candle.HighPrice >= stop)
{
BuyMarket();
ResetShortState();
return;
}
if (target > 0m && candle.LowPrice <= target)
{
BuyMarket();
ResetShortState();
return;
}
var trailingDistance = ConvertPointsToPrice(TrailingPoints);
if (trailingDistance <= 0m || _shortEntryPrice <= 0m)
return;
if (_shortEntryPrice - candle.ClosePrice >= trailingDistance)
{
var newStop = RoundPrice(candle.ClosePrice + trailingDistance);
if (_shortStop == 0m || newStop < _shortStop)
{
_shortStop = newStop;
if (candle.HighPrice >= _shortStop)
{
BuyMarket();
ResetShortState();
}
}
}
}
private void ResetPositionState()
{
ResetLongState();
ResetShortState();
}
private void ResetLongState()
{
_longEntryPrice = 0m;
_longStop = 0m;
_longTarget = 0m;
}
private void ResetShortState()
{
_shortEntryPrice = 0m;
_shortStop = 0m;
_shortTarget = 0m;
}
private void CalculatePivotLevels(ICandleMessage dailyCandle)
{
var high = dailyCandle.HighPrice;
var low = dailyCandle.LowPrice;
var close = dailyCandle.ClosePrice;
var pivot = (high + low + close) / 3m;
var metricDistance = MetricPoints * _priceStep;
var doubleMetric = 2m * metricDistance;
var twentyPoints = 20m * _priceStep;
var range = (high - low) * 1.1m / 2m;
var lwb = RoundPrice(pivot + metricDistance);
var lwr = RoundPrice(pivot - metricDistance);
var lrr = RoundPrice(pivot - doubleMetric);
var rtl = RoundPrice(Math.Max(close + range, lrr - twentyPoints));
var rts = RoundPrice(Math.Min(close - range, lrr - twentyPoints));
_longEntryLevel = lwb;
_shortEntryLevel = lwr;
_longStopLevel = lwr;
_shortStopLevel = lwb;
_longTargetLevel = rtl;
_shortTargetLevel = rts;
}
private decimal AdjustVolume(decimal volume)
{
if (Security == null)
return volume;
var step = Security.VolumeStep;
if (step is decimal volumeStep && volumeStep > 0m)
{
var steps = Math.Ceiling(volume / volumeStep);
if (steps < 1m)
steps = 1m;
volume = steps * volumeStep;
}
var minVolume = Security.MinVolume;
if (minVolume is decimal min && volume < min)
volume = min;
var maxVolume = Security.MaxVolume;
if (maxVolume is decimal max && max > 0m && volume > max)
volume = max;
return volume;
}
private decimal RoundPrice(decimal price)
{
if (_priceStep > 0m)
return Math.Round(price / _priceStep) * _priceStep;
return price;
}
private decimal ConvertPointsToPrice(int points)
{
if (points <= 0)
return 0m;
return points * _priceStep;
}
private bool IsWithinTradingHours(DateTimeOffset time)
{
var hour = time.Hour;
var start = Math.Clamp(StartHour, 0, 23);
var end = Math.Clamp(EndHour, 0, 23);
if (start <= end)
return hour >= start && hour <= end;
return hour >= start || hour <= end;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Math
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Strategies import Strategy
class wss_trader_strategy(Strategy):
"""Port of the 'Wss_trader' MetaTrader strategy built around Camarilla and classic
pivot levels. Uses dual timeframe: working candle for trading, daily candle for
pivot calculation. Time-filtered breakout entries with trailing stop."""
def __init__(self):
super(wss_trader_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(1))) \
.SetDisplay("Working Candle", "Primary candle type for trading logic", "General")
self._daily_candle_type = self.Param("DailyCandleType", DataType.TimeFrame(TimeSpan.FromHours(4))) \
.SetDisplay("Daily Candle", "Daily candle type used for pivot calculation", "General")
self._start_hour = self.Param("StartHour", 0) \
.SetDisplay("Start Hour", "Hour of day when trading becomes active (0-23)", "Session")
self._end_hour = self.Param("EndHour", 23) \
.SetDisplay("End Hour", "Hour of day after which trading is disabled (0-23)", "Session")
self._metric_points = self.Param("MetricPoints", 20) \
.SetGreaterThanZero() \
.SetDisplay("Metric Points", "Distance from pivot to entry levels in price steps", "Levels")
self._trailing_points = self.Param("TrailingPoints", 20) \
.SetDisplay("Trailing Points", "Trailing stop offset in price steps (0 disables trailing)", "Risk")
self._order_volume = self.Param("OrderVolume", 0.1) \
.SetGreaterThanZero() \
.SetDisplay("Order Volume", "Order volume for entries", "Orders")
self._previous_daily_candle_high = 0.0
self._previous_daily_candle_low = 0.0
self._previous_daily_candle_close = 0.0
self._has_previous_daily = False
self._price_step = 0.0
self._long_entry_level = 0.0
self._short_entry_level = 0.0
self._long_stop_level = 0.0
self._short_stop_level = 0.0
self._long_target_level = 0.0
self._short_target_level = 0.0
self._previous_close = 0.0
self._has_previous_close = False
self._levels_ready = False
self._can_trade = True
self._last_candle_open_time = None
self._long_entry_price = 0.0
self._short_entry_price = 0.0
self._long_stop = 0.0
self._short_stop = 0.0
self._long_target = 0.0
self._short_target = 0.0
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
@property
def DailyCandleType(self):
return self._daily_candle_type.Value
@DailyCandleType.setter
def DailyCandleType(self, value):
self._daily_candle_type.Value = value
@property
def StartHour(self):
return self._start_hour.Value
@property
def EndHour(self):
return self._end_hour.Value
@property
def MetricPoints(self):
return self._metric_points.Value
@property
def TrailingPoints(self):
return self._trailing_points.Value
@property
def OrderVolume(self):
return self._order_volume.Value
def OnReseted(self):
super(wss_trader_strategy, self).OnReseted()
self._previous_daily_candle_high = 0.0
self._previous_daily_candle_low = 0.0
self._previous_daily_candle_close = 0.0
self._has_previous_daily = False
self._price_step = 0.0
self._long_entry_level = 0.0
self._short_entry_level = 0.0
self._long_stop_level = 0.0
self._short_stop_level = 0.0
self._long_target_level = 0.0
self._short_target_level = 0.0
self._previous_close = 0.0
self._has_previous_close = False
self._levels_ready = False
self._can_trade = True
self._last_candle_open_time = None
self._long_entry_price = 0.0
self._short_entry_price = 0.0
self._long_stop = 0.0
self._short_stop = 0.0
self._long_target = 0.0
self._short_target = 0.0
def OnStarted2(self, time):
super(wss_trader_strategy, self).OnStarted2(time)
step = self.Security.PriceStep if self.Security is not None else 0.0
if step is None or float(step) <= 0:
step = 0.0001
self._price_step = float(step)
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self._process_candle).Start()
daily_subscription = self.SubscribeCandles(self.DailyCandleType)
daily_subscription.Bind(self._process_daily_candle).Start()
def _process_daily_candle(self, candle):
if candle.State != CandleStates.Finished:
return
if self._has_previous_daily:
self._calculate_pivot_levels(
self._previous_daily_candle_high,
self._previous_daily_candle_low,
self._previous_daily_candle_close
)
self._levels_ready = True
self._previous_daily_candle_high = float(candle.HighPrice)
self._previous_daily_candle_low = float(candle.LowPrice)
self._previous_daily_candle_close = float(candle.ClosePrice)
self._has_previous_daily = True
def _process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
if self._last_candle_open_time != candle.OpenTime:
self._can_trade = True
self._last_candle_open_time = candle.OpenTime
within_hours = self._is_within_trading_hours(candle.CloseTime)
if not within_hours:
if self.Position != 0:
if self.Position > 0:
self.SellMarket()
else:
self.BuyMarket()
self._reset_position_state()
self._previous_close = float(candle.ClosePrice)
self._has_previous_close = True
return
self._manage_positions(candle)
if not self._levels_ready:
self._previous_close = float(candle.ClosePrice)
self._has_previous_close = True
return
if not self._has_previous_close:
self._previous_close = float(candle.ClosePrice)
self._has_previous_close = True
return
if not self._can_trade or self.Position != 0:
self._previous_close = float(candle.ClosePrice)
return
close = float(candle.ClosePrice)
# Long breakout: previous close was below entry level, current crosses above
if self._previous_close < self._long_entry_level and close >= self._long_entry_level:
self.BuyMarket()
self._can_trade = False
self._long_entry_price = close
self._long_stop = self._round_price(self._long_stop_level)
self._long_target = self._round_price(self._long_target_level)
self._previous_close = close
return
# Short breakout: previous close was above entry level, current crosses below
if self._previous_close > self._short_entry_level and close <= self._short_entry_level:
self.SellMarket()
self._can_trade = False
self._short_entry_price = close
self._short_stop = self._round_price(self._short_stop_level)
self._short_target = self._round_price(self._short_target_level)
self._previous_close = close
return
self._previous_close = close
def _manage_positions(self, candle):
if self.Position > 0:
self._manage_long_position(candle)
elif self.Position < 0:
self._manage_short_position(candle)
def _manage_long_position(self, candle):
stop = self._long_stop
target = self._long_target
low = float(candle.LowPrice)
high = float(candle.HighPrice)
close = float(candle.ClosePrice)
if stop > 0 and low <= stop:
self.SellMarket()
self._reset_long_state()
return
if target > 0 and high >= target:
self.SellMarket()
self._reset_long_state()
return
trailing_distance = self._convert_points_to_price(self.TrailingPoints)
if trailing_distance <= 0 or self._long_entry_price <= 0:
return
if close - self._long_entry_price >= trailing_distance:
new_stop = self._round_price(close - trailing_distance)
if new_stop > self._long_stop:
self._long_stop = new_stop
if low <= self._long_stop:
self.SellMarket()
self._reset_long_state()
def _manage_short_position(self, candle):
stop = self._short_stop
target = self._short_target
high = float(candle.HighPrice)
low = float(candle.LowPrice)
close = float(candle.ClosePrice)
if stop > 0 and high >= stop:
self.BuyMarket()
self._reset_short_state()
return
if target > 0 and low <= target:
self.BuyMarket()
self._reset_short_state()
return
trailing_distance = self._convert_points_to_price(self.TrailingPoints)
if trailing_distance <= 0 or self._short_entry_price <= 0:
return
if self._short_entry_price - close >= trailing_distance:
new_stop = self._round_price(close + trailing_distance)
if self._short_stop == 0 or new_stop < self._short_stop:
self._short_stop = new_stop
if high >= self._short_stop:
self.BuyMarket()
self._reset_short_state()
def _reset_position_state(self):
self._reset_long_state()
self._reset_short_state()
def _reset_long_state(self):
self._long_entry_price = 0.0
self._long_stop = 0.0
self._long_target = 0.0
def _reset_short_state(self):
self._short_entry_price = 0.0
self._short_stop = 0.0
self._short_target = 0.0
def _calculate_pivot_levels(self, high, low, close):
pivot = (high + low + close) / 3.0
metric_distance = float(self.MetricPoints) * self._price_step
double_metric = 2.0 * metric_distance
twenty_points = 20.0 * self._price_step
range_val = (high - low) * 1.1 / 2.0
lwb = self._round_price(pivot + metric_distance)
lwr = self._round_price(pivot - metric_distance)
lrr = self._round_price(pivot - double_metric)
rtl = self._round_price(max(close + range_val, lrr - twenty_points))
rts = self._round_price(min(close - range_val, lrr - twenty_points))
self._long_entry_level = lwb
self._short_entry_level = lwr
self._long_stop_level = lwr
self._short_stop_level = lwb
self._long_target_level = rtl
self._short_target_level = rts
def _round_price(self, price):
if self._price_step > 0:
return round(price / self._price_step) * self._price_step
return price
def _convert_points_to_price(self, points):
if points <= 0:
return 0.0
return float(points) * self._price_step
def _is_within_trading_hours(self, time):
hour = time.Hour
start = max(0, min(23, self.StartHour))
end = max(0, min(23, self.EndHour))
if start <= end:
return hour >= start and hour <= end
return hour >= start or hour <= end
def CreateClone(self):
return wss_trader_strategy()