The Puncher 策略
概述
The Puncher 策略源自 MetaTrader 4 专家顾问 "The Puncher by L. Bigger",是一套反转型动量系统。策略同时使用慢速随机指标与经典 RSI,在两者共同指向极端超卖或超买时,于 K 线收盘后反向入场,从而捕捉潜在的回归行情。
交易逻辑
- 做多条件: 随机指标的信号线与 RSI 一同跌破超卖阈值。若持有空头仓位会先行平仓,随后开立新的多单。
- 做空条件: 当两者同时升破超买阈值时触发。若有多头仓位会先平仓,然后建立空单。
- 离场方式: 反向信号或保护规则(止损、止盈、保本、移动止损)都会导致平仓。
策略仅在所选时间框架的收盘 K 线运行,从而复刻原始 EA 的“在收盘交易”行为并降低噪音。
风险控制
- 止损 / 止盈: 以点数设定的固定距离,设为 0 时表示关闭。
- 保本: 当浮盈达到设定值后,将止损移动到开仓价。
- 移动止损: 价格每向有利方向移动足够距离时,按指定间距上调止损。
- 下单手数: 使用固定交易量参数,对应 MT4 中的 Lots 设置。
参数
| 参数 | 说明 | 默认值 |
|---|---|---|
OrderVolume |
新开仓的交易量。 | 1 |
StochasticLength |
随机指标 %K 的回溯长度。 | 100 |
StochasticSignalPeriod |
%K 进入信号线前的平滑周期。 | 3 |
StochasticSmoothingPeriod |
%D 信号线的平滑周期。 | 3 |
RsiPeriod |
RSI 指标的计算周期。 | 14 |
OversoldLevel |
判断超卖的共有阈值。 | 30 |
OverboughtLevel |
判断超买的共有阈值。 | 70 |
StopLossPips |
止损距离(0 表示禁用)。 | 2000 |
TakeProfitPips |
止盈距离(0 表示禁用)。 | 0 |
TrailingStopPips |
移动止损距离(0 表示禁用)。 | 0 |
TrailingStepPips |
每次调整移动止损所需的最小有利点数。 | 1 |
BreakEvenPips |
将止损移至保本所需的浮盈点数。 | 0 |
CandleType |
计算使用的 K 线类型。 | M15 |
说明
- 点值依据标的的最小报价步长或小数位推导,确保所有价格保护符合交易所精度要求。
- MT4 原版中的声音提示、邮件通知和图形标注属于平台特性,此处未实现。
- 策略可作为 StockSharp 环境下进一步实验与优化的基础模板。
using System;
using System.Collections.Generic;
using Ecng.Common;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Momentum-reversal strategy converted from the MetaTrader 4 expert advisor "The Puncher".
/// Trades when Stochastic and RSI simultaneously signal extreme oversold or overbought conditions
/// and manages positions with optional stop-loss, take-profit, break-even and trailing stop rules.
/// </summary>
public class ThePuncherStrategy : Strategy
{
private readonly StrategyParam<decimal> _orderVolume;
private readonly StrategyParam<int> _stochasticLength;
private readonly StrategyParam<int> _stochasticSignalPeriod;
private readonly StrategyParam<int> _stochasticSmoothingPeriod;
private readonly StrategyParam<int> _rsiPeriod;
private readonly StrategyParam<decimal> _oversoldLevel;
private readonly StrategyParam<decimal> _overboughtLevel;
private readonly StrategyParam<int> _stopLossPips;
private readonly StrategyParam<int> _takeProfitPips;
private readonly StrategyParam<int> _trailingStopPips;
private readonly StrategyParam<int> _trailingStepPips;
private readonly StrategyParam<int> _breakEvenPips;
private readonly StrategyParam<DataType> _candleType;
private decimal _pipSize;
private decimal _entryPrice;
private decimal? _stopPrice;
private decimal? _takeProfitPrice;
private decimal? _lastTrailingReference;
private bool _breakEvenActivated;
/// <summary>
/// Trade volume used for new market orders.
/// </summary>
public decimal OrderVolume
{
get => _orderVolume.Value;
set => _orderVolume.Value = value;
}
/// <summary>
/// Lookback length of the Stochastic oscillator.
/// </summary>
public int StochasticLength
{
get => _stochasticLength.Value;
set => _stochasticLength.Value = value;
}
/// <summary>
/// Smoothing period applied to %K before the signal line.
/// </summary>
public int StochasticSignalPeriod
{
get => _stochasticSignalPeriod.Value;
set => _stochasticSignalPeriod.Value = value;
}
/// <summary>
/// Smoothing period of the Stochastic signal line (%D).
/// </summary>
public int StochasticSmoothingPeriod
{
get => _stochasticSmoothingPeriod.Value;
set => _stochasticSmoothingPeriod.Value = value;
}
/// <summary>
/// RSI calculation period.
/// </summary>
public int RsiPeriod
{
get => _rsiPeriod.Value;
set => _rsiPeriod.Value = value;
}
/// <summary>
/// Shared oversold threshold for Stochastic and RSI.
/// </summary>
public decimal OversoldLevel
{
get => _oversoldLevel.Value;
set => _oversoldLevel.Value = value;
}
/// <summary>
/// Shared overbought threshold for Stochastic and RSI.
/// </summary>
public decimal OverboughtLevel
{
get => _overboughtLevel.Value;
set => _overboughtLevel.Value = value;
}
/// <summary>
/// Stop-loss distance in pips. Set to zero to disable.
/// </summary>
public int StopLossPips
{
get => _stopLossPips.Value;
set => _stopLossPips.Value = value;
}
/// <summary>
/// Take-profit distance in pips. Set to zero to disable.
/// </summary>
public int TakeProfitPips
{
get => _takeProfitPips.Value;
set => _takeProfitPips.Value = value;
}
/// <summary>
/// Trailing stop distance in pips. Set to zero to disable.
/// </summary>
public int TrailingStopPips
{
get => _trailingStopPips.Value;
set => _trailingStopPips.Value = value;
}
/// <summary>
/// Minimum favorable move before the trailing stop is tightened.
/// </summary>
public int TrailingStepPips
{
get => _trailingStepPips.Value;
set => _trailingStepPips.Value = value;
}
/// <summary>
/// Profit in pips required to move the stop to break-even.
/// </summary>
public int BreakEvenPips
{
get => _breakEvenPips.Value;
set => _breakEvenPips.Value = value;
}
/// <summary>
/// Candle type used for indicator calculations.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initializes a new instance of the strategy.
/// </summary>
public ThePuncherStrategy()
{
_orderVolume = Param(nameof(OrderVolume), 1m)
.SetDisplay("Order Volume", "Default volume for new entries", "Trading")
.SetGreaterThanZero();
_stochasticLength = Param(nameof(StochasticLength), 100)
.SetDisplay("Stochastic Length", "Lookback period for %K", "Indicators")
.SetGreaterThanZero()
.SetOptimize(50, 150, 10);
_stochasticSignalPeriod = Param(nameof(StochasticSignalPeriod), 3)
.SetDisplay("Stochastic Signal", "Smoothing period for %K", "Indicators")
.SetGreaterThanZero()
.SetOptimize(1, 10, 1);
_stochasticSmoothingPeriod = Param(nameof(StochasticSmoothingPeriod), 3)
.SetDisplay("Stochastic %D", "Smoothing period for %D", "Indicators")
.SetGreaterThanZero()
.SetOptimize(1, 10, 1);
_rsiPeriod = Param(nameof(RsiPeriod), 14)
.SetDisplay("RSI Period", "Calculation period for RSI", "Indicators")
.SetGreaterThanZero()
.SetOptimize(7, 28, 1);
_oversoldLevel = Param(nameof(OversoldLevel), 30m)
.SetDisplay("Oversold Level", "Shared oversold threshold", "Indicators")
.SetRange(0m, 100m)
.SetOptimize(10m, 40m, 5m);
_overboughtLevel = Param(nameof(OverboughtLevel), 70m)
.SetDisplay("Overbought Level", "Shared overbought threshold", "Indicators")
.SetRange(0m, 100m)
.SetOptimize(60m, 90m, 5m);
_stopLossPips = Param(nameof(StopLossPips), 2000)
.SetDisplay("Stop-Loss (pips)", "Protective stop distance", "Risk")
.SetOptimize(200, 3000, 200);
_takeProfitPips = Param(nameof(TakeProfitPips), 0)
.SetDisplay("Take-Profit (pips)", "Profit target distance", "Risk")
.SetOptimize(0, 3000, 200);
_trailingStopPips = Param(nameof(TrailingStopPips), 0)
.SetDisplay("Trailing Stop (pips)", "Trailing stop distance", "Risk")
.SetOptimize(0, 2000, 100);
_trailingStepPips = Param(nameof(TrailingStepPips), 1)
.SetDisplay("Trailing Step (pips)", "Minimum move before trailing", "Risk")
.SetNotNegative()
.SetOptimize(0, 500, 10);
_breakEvenPips = Param(nameof(BreakEvenPips), 0)
.SetDisplay("Break-Even (pips)", "Profit required to move stop to entry", "Risk")
.SetNotNegative()
.SetOptimize(0, 1000, 50);
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(15).TimeFrame())
.SetDisplay("Candle Type", "Primary timeframe for signals", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
ResetTradeState();
_pipSize = 0m;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
Volume = OrderVolume;
_pipSize = CalculatePipSize();
var stochastic = new StochasticOscillator();
stochastic.K.Length = StochasticLength;
stochastic.D.Length = StochasticSmoothingPeriod;
var rsi = new RelativeStrengthIndex { Length = RsiPeriod };
var subscription = SubscribeCandles(CandleType);
subscription
.BindEx(stochastic, rsi, ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, stochastic);
DrawIndicator(area, rsi);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle, IIndicatorValue stochasticValue, IIndicatorValue rsiValue)
{
if (candle.State != CandleStates.Finished)
return;
if (!IsFormedAndOnlineAndAllowTrading())
return;
if (!stochasticValue.IsFinal || !rsiValue.IsFinal)
return;
var stochastic = (StochasticOscillatorValue)stochasticValue;
if (stochastic.D is not decimal stochSignal)
return;
var rsi = rsiValue.ToDecimal();
UpdateEntryPriceFromPosition();
var buySignal = stochSignal < OversoldLevel && rsi < OversoldLevel;
var sellSignal = stochSignal > OverboughtLevel && rsi > OverboughtLevel;
if (HandleActivePosition(candle, buySignal, sellSignal))
return;
if (Position == 0)
{
if (buySignal)
{
EnterLong(candle.ClosePrice);
}
else if (sellSignal)
{
EnterShort(candle.ClosePrice);
}
}
}
private bool HandleActivePosition(ICandleMessage candle, bool buySignal, bool sellSignal)
{
if (Position > 0)
{
if (TryExitLongByProtection(candle))
return true;
ApplyLongRiskManagement(candle);
if (sellSignal)
{
SellMarket(Position);
ResetTradeState();
return true;
}
}
else if (Position < 0)
{
if (TryExitShortByProtection(candle))
return true;
ApplyShortRiskManagement(candle);
if (buySignal)
{
BuyMarket(Math.Abs(Position));
ResetTradeState();
return true;
}
}
return false;
}
private bool TryExitLongByProtection(ICandleMessage candle)
{
if (Position <= 0)
return false;
if (_stopPrice.HasValue && candle.LowPrice <= _stopPrice.Value)
{
SellMarket(Position);
ResetTradeState();
return true;
}
if (_takeProfitPrice.HasValue && candle.HighPrice >= _takeProfitPrice.Value)
{
SellMarket(Position);
ResetTradeState();
return true;
}
return false;
}
private bool TryExitShortByProtection(ICandleMessage candle)
{
if (Position >= 0)
return false;
if (_stopPrice.HasValue && candle.HighPrice >= _stopPrice.Value)
{
BuyMarket(Math.Abs(Position));
ResetTradeState();
return true;
}
if (_takeProfitPrice.HasValue && candle.LowPrice <= _takeProfitPrice.Value)
{
BuyMarket(Math.Abs(Position));
ResetTradeState();
return true;
}
return false;
}
private void ApplyLongRiskManagement(ICandleMessage candle)
{
var close = candle.ClosePrice;
if (_pipSize > 0m && BreakEvenPips > 0 && !_breakEvenActivated && _entryPrice > 0m)
{
var breakEvenDistance = BreakEvenPips * _pipSize;
if (close - _entryPrice >= breakEvenDistance)
{
var breakEvenPrice = _entryPrice;
if (!_stopPrice.HasValue || _stopPrice.Value < breakEvenPrice)
_stopPrice = breakEvenPrice;
_breakEvenActivated = true;
}
}
if (_pipSize > 0m && TrailingStopPips > 0)
{
var trailingDistance = TrailingStopPips * _pipSize;
var trailingStep = TrailingStepPips * _pipSize;
var reference = _lastTrailingReference ?? _entryPrice;
var shouldUpdate = trailingStep <= 0m || close - reference >= trailingStep;
if (shouldUpdate && close - _entryPrice > trailingDistance)
{
var newStop = close - trailingDistance;
if (!_stopPrice.HasValue || newStop > _stopPrice.Value)
_stopPrice = newStop;
_lastTrailingReference = close;
}
}
}
private void ApplyShortRiskManagement(ICandleMessage candle)
{
var close = candle.ClosePrice;
if (_pipSize > 0m && BreakEvenPips > 0 && !_breakEvenActivated && _entryPrice > 0m)
{
var breakEvenDistance = BreakEvenPips * _pipSize;
if (_entryPrice - close >= breakEvenDistance)
{
var breakEvenPrice = _entryPrice;
if (!_stopPrice.HasValue || _stopPrice.Value > breakEvenPrice)
_stopPrice = breakEvenPrice;
_breakEvenActivated = true;
}
}
if (_pipSize > 0m && TrailingStopPips > 0)
{
var trailingDistance = TrailingStopPips * _pipSize;
var trailingStep = TrailingStepPips * _pipSize;
var reference = _lastTrailingReference ?? _entryPrice;
var shouldUpdate = trailingStep <= 0m || reference - close >= trailingStep;
if (shouldUpdate && _entryPrice - close > trailingDistance)
{
var newStop = close + trailingDistance;
if (!_stopPrice.HasValue || newStop < _stopPrice.Value)
_stopPrice = newStop;
_lastTrailingReference = close;
}
}
}
private void EnterLong(decimal referencePrice)
{
BuyMarket();
_entryPrice = referencePrice;
InitializeProtectionLevels(isLong: true);
}
private void EnterShort(decimal referencePrice)
{
SellMarket();
_entryPrice = referencePrice;
InitializeProtectionLevels(isLong: false);
}
private void InitializeProtectionLevels(bool isLong)
{
_stopPrice = null;
_takeProfitPrice = null;
_lastTrailingReference = null;
_breakEvenActivated = false;
if (_pipSize <= 0m)
return;
var stopDistance = StopLossPips > 0 ? StopLossPips * _pipSize : 0m;
var takeDistance = TakeProfitPips > 0 ? TakeProfitPips * _pipSize : 0m;
if (isLong)
{
if (stopDistance > 0m)
_stopPrice = _entryPrice - stopDistance;
if (takeDistance > 0m)
_takeProfitPrice = _entryPrice + takeDistance;
}
else
{
if (stopDistance > 0m)
_stopPrice = _entryPrice + stopDistance;
if (takeDistance > 0m)
_takeProfitPrice = _entryPrice - takeDistance;
}
}
private void UpdateEntryPriceFromPosition()
{
// Entry price is tracked manually via _entryPrice field
}
private void ResetTradeState()
{
_entryPrice = 0m;
_stopPrice = null;
_takeProfitPrice = null;
_lastTrailingReference = null;
_breakEvenActivated = false;
}
private decimal CalculatePipSize()
{
var step = Security?.PriceStep ?? 0m;
if (step > 0m)
return step;
var decimals = Security?.Decimals ?? 0;
if (decimals > 0)
{
var value = 1m;
for (var i = 0; i < decimals; i++)
value /= 10m;
return value;
}
return 0.0001m;
}
}
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.Indicators import StochasticOscillator, RelativeStrengthIndex
from StockSharp.Algo.Strategies import Strategy
from datatype_extensions import *
from indicator_extensions import *
class the_puncher_strategy(Strategy):
"""Stochastic + RSI extreme oversold/overbought with SL/TP/trailing/break-even."""
def __init__(self):
super(the_puncher_strategy, self).__init__()
self._stoch_length = self.Param("StochasticLength", 100).SetGreaterThanZero().SetDisplay("Stochastic Length", "Lookback for %K", "Indicators")
self._stoch_d = self.Param("StochasticSmoothingPeriod", 3).SetGreaterThanZero().SetDisplay("Stochastic %D", "Smoothing for %D", "Indicators")
self._rsi_period = self.Param("RsiPeriod", 14).SetGreaterThanZero().SetDisplay("RSI Period", "RSI period", "Indicators")
self._oversold = self.Param("OversoldLevel", 30.0).SetDisplay("Oversold Level", "Shared oversold threshold", "Indicators")
self._overbought = self.Param("OverboughtLevel", 70.0).SetDisplay("Overbought Level", "Shared overbought threshold", "Indicators")
self._sl_pips = self.Param("StopLossPips", 2000).SetDisplay("Stop-Loss (pips)", "Protective stop distance", "Risk")
self._tp_pips = self.Param("TakeProfitPips", 0).SetDisplay("Take-Profit (pips)", "Profit target distance", "Risk")
self._trail_pips = self.Param("TrailingStopPips", 0).SetDisplay("Trailing Stop (pips)", "Trailing stop distance", "Risk")
self._trail_step = self.Param("TrailingStepPips", 1).SetNotNegative().SetDisplay("Trailing Step (pips)", "Min move before trailing", "Risk")
self._be_pips = self.Param("BreakEvenPips", 0).SetNotNegative().SetDisplay("Break-Even (pips)", "Profit to move stop to entry", "Risk")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(15))).SetDisplay("Candle Type", "Primary timeframe", "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(the_puncher_strategy, self).OnReseted()
self._entry_price = 0
self._stop_price = 0
self._tp_price = 0
self._be_activated = False
def OnStarted2(self, time):
super(the_puncher_strategy, self).OnStarted2(time)
self._entry_price = 0
self._stop_price = 0
self._tp_price = 0
self._be_activated = False
self._stoch = StochasticOscillator()
self._stoch.K.Length = self._stoch_length.Value
self._stoch.D.Length = self._stoch_d.Value
rsi = RelativeStrengthIndex()
rsi.Length = self._rsi_period.Value
sub = self.SubscribeCandles(self.CandleType)
sub.BindEx(self._stoch, rsi, self.OnProcess).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, sub)
self.DrawIndicator(area, self._stoch)
self.DrawOwnTrades(area)
def OnProcess(self, candle, stoch_val, rsi_val):
if candle.State != CandleStates.Finished:
return
# Extract %D from stochastic
stoch_signal = None
inner = stoch_val.InnerValues
if inner is not None:
for iv in inner:
stoch_signal = float(iv.Value)
break
if stoch_signal is None:
return
rsi = float(rsi_val)
close = float(candle.ClosePrice)
high = float(candle.HighPrice)
low = float(candle.LowPrice)
oversold = self._oversold.Value
overbought = self._overbought.Value
buy_signal = stoch_signal < oversold and rsi < oversold
sell_signal = stoch_signal > overbought and rsi > overbought
# Manage position
if self.Position > 0:
if self._stop_price > 0 and low <= self._stop_price:
self.SellMarket()
self._reset_state()
return
if self._tp_price > 0 and high >= self._tp_price:
self.SellMarket()
self._reset_state()
return
self._apply_long_risk(close)
if sell_signal:
self.SellMarket()
self._reset_state()
return
elif self.Position < 0:
if self._stop_price > 0 and high >= self._stop_price:
self.BuyMarket()
self._reset_state()
return
if self._tp_price > 0 and low <= self._tp_price:
self.BuyMarket()
self._reset_state()
return
self._apply_short_risk(close)
if buy_signal:
self.BuyMarket()
self._reset_state()
return
# Entry
if self.Position == 0:
if buy_signal:
self.BuyMarket()
self._entry_price = close
self._init_protection(True)
elif sell_signal:
self.SellMarket()
self._entry_price = close
self._init_protection(False)
def _init_protection(self, is_long):
self._stop_price = 0
self._tp_price = 0
self._be_activated = False
sl = self._sl_pips.Value
tp = self._tp_pips.Value
if is_long:
if sl > 0:
self._stop_price = self._entry_price - sl
if tp > 0:
self._tp_price = self._entry_price + tp
else:
if sl > 0:
self._stop_price = self._entry_price + sl
if tp > 0:
self._tp_price = self._entry_price - tp
def _apply_long_risk(self, close):
be = self._be_pips.Value
if be > 0 and not self._be_activated and self._entry_price > 0:
if close - self._entry_price >= be:
if self._stop_price == 0 or self._stop_price < self._entry_price:
self._stop_price = self._entry_price
self._be_activated = True
trail = self._trail_pips.Value
step = self._trail_step.Value
if trail > 0 and close - self._entry_price > trail:
new_stop = close - trail
if self._stop_price == 0 or new_stop > self._stop_price:
self._stop_price = new_stop
def _apply_short_risk(self, close):
be = self._be_pips.Value
if be > 0 and not self._be_activated and self._entry_price > 0:
if self._entry_price - close >= be:
if self._stop_price == 0 or self._stop_price > self._entry_price:
self._stop_price = self._entry_price
self._be_activated = True
trail = self._trail_pips.Value
step = self._trail_step.Value
if trail > 0 and self._entry_price - close > trail:
new_stop = close + trail
if self._stop_price == 0 or new_stop < self._stop_price:
self._stop_price = new_stop
def _reset_state(self):
self._entry_price = 0
self._stop_price = 0
self._tp_price = 0
self._be_activated = False
def CreateClone(self):
return the_puncher_strategy()