Lucky Code — это скальпер, перенесённый с оригинального советника MetaTrader «Lucky_code». Стратегия следит за экстремальными изменениями между лучшим бидом и аском и реагирует, когда спрэд расширяется или сужается на заданное количество пунктов. Сделки сопровождаются агрессивным управлением: прибыль фиксируется сразу после выгодного тика, а убыток ограничивается при превышении допустимого отклонения.
Данные и исполнение
Рыночные данные: необходим поток котировок Level 1, предоставляющий актуальные значения лучшего bida и aska.
Типы заявок: для входа и выхода используются рыночные приказы, что повторяет тик-ориентированную логику версии на MQL.
Режим позиций: подходит как для неттинговых, так и для хеджевых счетов. Несколько сделок суммируются в единую нетто-позицию и управляются общим блоком.
Параметры
Shift points — минимальное количество пунктов между последовательными котировками, при котором допускается новый вход. Чем выше значение, тем меньше сделок и шума.
Limit points — максимально допустимое неблагоприятное смещение цены, после которого позиция принудительно закрывается. Параметр переводится в цену с учётом шага тика инструмента.
Логика торговли
Инициализация
Конвертирует параметр в пунктах в денежный эквивалент через шаг цены инструмента.
Подписывается на поток Level 1 и обнуляет внутренние переменные последнего bida/aska.
Правила входа
Если лучший ask вырос минимум на заданный шаг относительно предыдущего значения, стратегия открывает короткую позицию (аналогично исходному советнику, который продавал на всплесках).
Если лучший bid упал на тот же шаг ниже предыдущего значения, стратегия открывает длинную позицию в расчёте на возврат.
Размер позиции
Базовый объём берётся из свойства Volume стратегии.
При наличии стоимости портфеля объём увеличивается до round(Equity / 10 000, 1) лота, что соответствует расчёту AccountFreeMargin/10000 в MetaTrader.
Правила выхода
Длинная позиция закрывается сразу, как только bid превышает среднюю цену входа, либо если ask снизился от цены входа на величину лимита.
Короткая позиция закрывается, когда ask опускается ниже цены входа, либо когда bid поднимается выше на заданный лимит.
Практические замечания
Стратегия реагирует на каждый тик, поэтому на «шумных» потоках имеет смысл повышать параметр Shift или фильтровать данные.
Рыночные заявки требуют достаточной ликвидности, иначе всплески спрэда могут привести к нежелательным проскальзываниям.
Рекомендуется дополнить стратегию портфельными ограничениями: дневной стоп, лимит по просадке, контроль максимального количества открытых сделок.
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;
using StockSharp.Algo;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Momentum strategy that opens trades when candle price jumps reach a configurable distance and manages exits with profit and drawdown filters.
/// </summary>
public class LuckyCodeStrategy : Strategy
{
private readonly StrategyParam<int> _shiftPoints;
private readonly StrategyParam<int> _limitPoints;
private decimal? _previousClose;
private decimal _shiftThreshold;
private decimal _limitThreshold;
private decimal _entryPrice;
private bool _thresholdsReady;
private int _holdBars;
/// <summary>
/// Minimum price movement in points required before opening a new trade.
/// </summary>
public int ShiftPoints
{
get => _shiftPoints.Value;
set => _shiftPoints.Value = value;
}
/// <summary>
/// Maximum adverse excursion in points tolerated before forcing an exit.
/// </summary>
public int LimitPoints
{
get => _limitPoints.Value;
set => _limitPoints.Value = value;
}
/// <summary>
/// Initializes strategy parameters.
/// </summary>
public LuckyCodeStrategy()
{
_shiftPoints = Param(nameof(ShiftPoints), 3)
.SetGreaterThanZero()
.SetDisplay("Shift points", "Minimum price jump required to trigger entries", "Trading")
.SetOptimize(1, 20, 1);
_limitPoints = Param(nameof(LimitPoints), 18)
.SetGreaterThanZero()
.SetDisplay("Limit points", "Maximum number of points allowed against the position", "Risk management")
.SetOptimize(5, 100, 5);
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_previousClose = null;
_shiftThreshold = 0m;
_limitThreshold = 0m;
_entryPrice = 0m;
_thresholdsReady = false;
_holdBars = 0;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
// Subscribe to candles and process each finished candle.
var tf = TimeSpan.FromMinutes(5).TimeFrame();
SubscribeCandles(tf)
.Bind(ProcessCandle)
.Start();
}
private void EnsureThresholds(decimal price)
{
if (_thresholdsReady)
return;
if (price <= 0m)
return;
// Use percentage of price. ShiftPoints=3 means 3% shift, LimitPoints=18 means 18% limit.
_shiftThreshold = price * ShiftPoints * 0.01m;
_limitThreshold = price * LimitPoints * 0.01m;
_thresholdsReady = true;
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
var close = candle.ClosePrice;
EnsureThresholds(close);
if (!_thresholdsReady)
return;
// Count hold bars for position management.
if (Position != 0)
_holdBars++;
if (_previousClose is decimal prevClose)
{
var delta = close - prevClose;
// Only enter if flat.
if (Position == 0)
{
// Price dropped sharply -> buy on expected rebound.
if (-delta >= _shiftThreshold)
{
BuyMarket();
_entryPrice = close;
_holdBars = 0;
LogInfo($"Buy triggered by fast price drop. Price={close:0.#####}");
}
// Price rose sharply -> sell on expected reversal.
else if (delta >= _shiftThreshold)
{
SellMarket();
_entryPrice = close;
_holdBars = 0;
LogInfo($"Sell triggered by fast price rise. Price={close:0.#####}");
}
}
}
_previousClose = close;
TryClosePosition(close);
}
private void TryClosePosition(decimal currentPrice)
{
if (Position == 0)
return;
var avgPrice = _entryPrice;
if (avgPrice <= 0m)
return;
// Minimum hold of 3 bars before checking exit.
if (_holdBars < 3)
return;
// Use half of shift threshold as profit target.
var profitTarget = _shiftThreshold * 0.5m;
if (Position > 0)
{
// Close long on profit target or drawdown limit.
if (currentPrice - avgPrice >= profitTarget)
{
SellMarket();
_holdBars = 0;
LogInfo($"Closed long on profit. Price={currentPrice:0.#####}");
}
else if (_limitThreshold > 0m && avgPrice - currentPrice >= _limitThreshold)
{
SellMarket();
_holdBars = 0;
LogInfo($"Closed long on drawdown limit. Price={currentPrice:0.#####}");
}
}
else if (Position < 0)
{
// Close short on profit target or drawdown limit.
if (avgPrice - currentPrice >= profitTarget)
{
BuyMarket();
_holdBars = 0;
LogInfo($"Closed short on profit. Price={currentPrice:0.#####}");
}
else if (_limitThreshold > 0m && currentPrice - avgPrice >= _limitThreshold)
{
BuyMarket();
_holdBars = 0;
LogInfo($"Closed short on drawdown limit. Price={currentPrice:0.#####}");
}
}
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Strategies import Strategy
class lucky_code_strategy(Strategy):
"""Momentum strategy that opens trades when candle price jumps reach a configurable distance
and manages exits with profit and drawdown filters."""
def __init__(self):
super(lucky_code_strategy, self).__init__()
self._shift_points = self.Param("ShiftPoints", 3) \
.SetGreaterThanZero() \
.SetDisplay("Shift points", "Minimum price jump required to trigger entries", "Trading") \
.SetOptimize(1, 20, 1)
self._limit_points = self.Param("LimitPoints", 18) \
.SetGreaterThanZero() \
.SetDisplay("Limit points", "Maximum number of points allowed against the position", "Risk management") \
.SetOptimize(5, 100, 5)
self._previous_close = None
self._shift_threshold = 0.0
self._limit_threshold = 0.0
self._entry_price = 0.0
self._thresholds_ready = False
self._hold_bars = 0
@property
def ShiftPoints(self):
return self._shift_points.Value
@ShiftPoints.setter
def ShiftPoints(self, value):
self._shift_points.Value = value
@property
def LimitPoints(self):
return self._limit_points.Value
@LimitPoints.setter
def LimitPoints(self, value):
self._limit_points.Value = value
def OnReseted(self):
super(lucky_code_strategy, self).OnReseted()
self._previous_close = None
self._shift_threshold = 0.0
self._limit_threshold = 0.0
self._entry_price = 0.0
self._thresholds_ready = False
self._hold_bars = 0
def OnStarted2(self, time):
super(lucky_code_strategy, self).OnStarted2(time)
tf = DataType.TimeFrame(TimeSpan.FromMinutes(5))
subscription = self.SubscribeCandles(tf)
subscription.Bind(self._process_candle).Start()
def _ensure_thresholds(self, price):
if self._thresholds_ready:
return
if price <= 0.0:
return
# Use percentage of price. ShiftPoints=3 means 3% shift, LimitPoints=18 means 18% limit.
self._shift_threshold = price * self.ShiftPoints * 0.01
self._limit_threshold = price * self.LimitPoints * 0.01
self._thresholds_ready = True
def _process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
close = float(candle.ClosePrice)
self._ensure_thresholds(close)
if not self._thresholds_ready:
return
# Count hold bars for position management.
if self.Position != 0:
self._hold_bars += 1
if self._previous_close is not None:
prev_close = self._previous_close
delta = close - prev_close
# Only enter if flat.
if self.Position == 0:
# Price dropped sharply -> buy on expected rebound.
if (-delta) >= self._shift_threshold:
self.BuyMarket()
self._entry_price = close
self._hold_bars = 0
self.LogInfo("Buy triggered by fast price drop. Price=" + str(close))
# Price rose sharply -> sell on expected reversal.
elif delta >= self._shift_threshold:
self.SellMarket()
self._entry_price = close
self._hold_bars = 0
self.LogInfo("Sell triggered by fast price rise. Price=" + str(close))
self._previous_close = close
self._try_close_position(close)
def _try_close_position(self, current_price):
if self.Position == 0:
return
avg_price = self._entry_price
if avg_price <= 0.0:
return
# Minimum hold of 3 bars before checking exit.
if self._hold_bars < 3:
return
# Use half of shift threshold as profit target.
profit_target = self._shift_threshold * 0.5
if self.Position > 0:
# Close long on profit target or drawdown limit.
if current_price - avg_price >= profit_target:
self.SellMarket()
self._hold_bars = 0
self.LogInfo("Closed long on profit. Price=" + str(current_price))
elif self._limit_threshold > 0.0 and avg_price - current_price >= self._limit_threshold:
self.SellMarket()
self._hold_bars = 0
self.LogInfo("Closed long on drawdown limit. Price=" + str(current_price))
elif self.Position < 0:
# Close short on profit target or drawdown limit.
if avg_price - current_price >= profit_target:
self.BuyMarket()
self._hold_bars = 0
self.LogInfo("Closed short on profit. Price=" + str(current_price))
elif self._limit_threshold > 0.0 and current_price - avg_price >= self._limit_threshold:
self.BuyMarket()
self._hold_bars = 0
self.LogInfo("Closed short on drawdown limit. Price=" + str(current_price))
def CreateClone(self):
return lucky_code_strategy()