在 GitHub 上查看
Aeron JJN Scalper EA 策略
概述
本策略是 Aeron JJN Scalper 专家的 StockSharp 高级 API 版本。策略仅处理已结束的K线,寻找特定的两根K线反转结构,并把上一根强劲反向K线的开盘价作为模拟挂单触发价。一旦行情触及该价位,策略以市价建仓,同时根据 ATR 设置止损、止盈,并使用基于点差的追踪止损进行仓位管理。
核心思想:
- 方向由两根K线组成的看涨/看跌反转形态决定。
- 入场价来自最近一根满足条件的反向K线开盘价。
- 信号K线的 ATR(8) 数值同时决定止损和止盈距离。
- 追踪止损根据配置的点差阈值逐步向盈利方向移动保护价位。
- 挂单价位在超过设定分钟数后自动失效。
交易规则
信号识别
- 仅在配置的时间框架(默认 1 分钟)内处理已收盘的K线。
- 根据交易品种的最小价格步长计算点值,对于 3 位或 5 位报价额外乘以 10,以模拟 MetaTrader 的点差定义。
- 保存最近 120 根K线,用于寻找参考K线。
- 做多信号 条件:
- 当前K线收盘价高于开盘价;
- 前一根K线为阴线,实体大于
DojiDiff1Pips;
- 向后查找最近一根实体大于
DojiDiff2Pips 的阴线,其开盘价作为买入触发价。
- 做空信号 条件:
- 当前K线收盘价低于开盘价;
- 前一根K线为阳线,实体大于
DojiDiff1Pips;
- 向后查找最近一根实体大于
DojiDiff2Pips 的阳线,其开盘价作为卖出触发价。
- 当同方向已存在待触发价位或 ATR 尚未形成时忽略新的信号。
挂单价位管理
- 触发价位被视为挂单,如果价格始终未触及并超过
ResetMinutes 指定的有效期,将被自动取消。
- 后续K线出现最高价 ≥ 买入价位(或最低价 ≤ 卖出价位)时,策略会发送市价单,在需要时平掉反向持仓并加上
Volume 的净头寸。
- 建仓成功后会清除另一方向仍然存在的挂单价位。
止损、止盈与追踪
- 建仓时记录信号K线的 ATR(8):
- 多单:止损 =
entry - ATR,止盈 = entry + ATR;
- 空单:止损 =
entry + ATR,止盈 = entry - ATR。
- 每根完成的K线均会:
- 检查价格是否触及止损或止盈,触发后以市价平仓;
- 当浮盈至少达到
TrailingStopPips + TrailingStepPips 点时启动追踪,将保护价位更新为距离最新收盘价 TrailingStopPips 的位置,并且只朝盈利方向移动。
- 若仓位被手动关闭,内部状态会自动重置。
参数
| 参数 |
默认值 |
说明 |
Volume |
0.1 |
入场净头寸。若需要反向,策略会在此基础上加上当前持仓绝对值。 |
TrailingStopPips |
5 |
追踪止损的基础点差(会转换为价格单位)。 |
TrailingStepPips |
5 |
每次移动追踪止损前,价格需额外前进的点差。 |
ResetMinutes |
10 |
挂单价位的有效时间(分钟)。 |
DojiDiff1Pips |
10 |
触发信号的前一根K线最小实体(点差)。 |
DojiDiff2Pips |
4 |
用作入场参考的K线最小实体(点差)。 |
CandleType |
1 分钟 |
用于计算的K线类型。 |
实现说明
- 策略完全基于已收盘K线运行,并在内存中保存挂单价位;当触发价被突破时,通过市价单完成入场,从而在高层 API 中模拟原始 EA 的逻辑。
- 使用
AverageTrueRange 计算 ATR(8),保证每笔交易的止损与止盈距离固定。
- 点值转换沿用了 MetaTrader 对 3/5 位报价的处理;若品种缺少
PriceStep 则使用默认值 1。
- 维护 120 根历史K线,对应原策略
CopyRates 的 100 根窗口并保留余量。
- 本版本未提供 Python 实现。
使用方法
- 将策略附加到目标证券与组合上。
- 根据品种调整时间框架、点差参数以及 ATR 条件。
- 启动策略后,它会自动跟踪信号、在触发价被突破时以市价入场,并负责止盈、止损及追踪管理。
using System;
using System.Collections.Generic;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Port of the Aeron JJN Scalper expert advisor.
/// Detects reversal candle patterns (bullish candle after strong bearish, and vice versa)
/// and enters at breakout of the prior candle's open level.
/// </summary>
public class AeronJjnScalperEaStrategy : Strategy
{
private readonly StrategyParam<int> _atrLength;
private readonly StrategyParam<decimal> _bodyMinAtr;
private readonly StrategyParam<int> _cooldownBars;
private readonly StrategyParam<DataType> _candleType;
private AverageTrueRange _atr;
private decimal _prevOpen;
private decimal _prevClose;
private bool _hasPrev;
private int _cooldown;
public int AtrLength
{
get => _atrLength.Value;
set => _atrLength.Value = value;
}
public decimal BodyMinAtr
{
get => _bodyMinAtr.Value;
set => _bodyMinAtr.Value = value;
}
public int CooldownBars
{
get => _cooldownBars.Value;
set => _cooldownBars.Value = value;
}
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
public AeronJjnScalperEaStrategy()
{
_atrLength = Param(nameof(AtrLength), 14)
.SetGreaterThanZero()
.SetDisplay("ATR Length", "ATR indicator period", "Indicators");
_bodyMinAtr = Param(nameof(BodyMinAtr), 1.5m)
.SetDisplay("Body Min ATR", "Minimum candle body size as ATR multiple", "Indicators");
_cooldownBars = Param(nameof(CooldownBars), 10)
.SetGreaterThanZero()
.SetDisplay("Cooldown Bars", "Bars to wait between trades", "Trading");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Candle Type", "General");
}
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
=> [(Security, CandleType)];
protected override void OnReseted()
{
base.OnReseted();
_hasPrev = false;
_cooldown = 0;
_prevOpen = 0;
_prevClose = 0;
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_atr = new AverageTrueRange { Length = AtrLength };
_hasPrev = false;
_cooldown = 0;
var subscription = SubscribeCandles(CandleType);
subscription.Bind(_atr, ProcessCandle).Start();
StartProtection(
takeProfit: new Unit(2, UnitTypes.Percent),
stopLoss: new Unit(1, UnitTypes.Percent)
);
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle, decimal atrVal)
{
if (candle.State != CandleStates.Finished)
return;
if (atrVal <= 0)
{
SavePrev(candle);
return;
}
if (_cooldown > 0)
_cooldown--;
if (_hasPrev && _cooldown == 0 && Position == 0)
{
var prevBody = Math.Abs(_prevClose - _prevOpen);
var minBody = atrVal * BodyMinAtr;
// Bullish candle after strong bearish candle => buy
if (candle.ClosePrice > candle.OpenPrice && _prevClose < _prevOpen && prevBody >= minBody)
{
BuyMarket();
_cooldown = CooldownBars;
}
// Bearish candle after strong bullish candle => sell
else if (candle.ClosePrice < candle.OpenPrice && _prevClose > _prevOpen && prevBody >= minBody)
{
SellMarket();
_cooldown = CooldownBars;
}
}
SavePrev(candle);
}
private void SavePrev(ICandleMessage candle)
{
_prevOpen = candle.OpenPrice;
_prevClose = candle.ClosePrice;
_hasPrev = true;
}
}
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, Unit, UnitTypes
from StockSharp.Algo.Indicators import AverageTrueRange
from StockSharp.Algo.Strategies import Strategy
class aeron_jjn_scalper_ea_strategy(Strategy):
def __init__(self):
super(aeron_jjn_scalper_ea_strategy, self).__init__()
self._atr_length = self.Param("AtrLength", 14)
self._body_min_atr = self.Param("BodyMinAtr", 1.5)
self._cooldown_bars = self.Param("CooldownBars", 10)
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5)))
self._prev_open = 0.0
self._prev_close = 0.0
self._has_prev = False
self._cooldown = 0
@property
def AtrLength(self):
return self._atr_length.Value
@property
def BodyMinAtr(self):
return self._body_min_atr.Value
@property
def CooldownBars(self):
return self._cooldown_bars.Value
@property
def CandleType(self):
return self._candle_type.Value
def OnStarted2(self, time):
super(aeron_jjn_scalper_ea_strategy, self).OnStarted2(time)
self._has_prev = False
self._cooldown = 0
atr = AverageTrueRange()
atr.Length = self.AtrLength
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(atr, self._process_candle).Start()
self.StartProtection(
takeProfit=Unit(2, UnitTypes.Percent),
stopLoss=Unit(1, UnitTypes.Percent)
)
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawOwnTrades(area)
def _process_candle(self, candle, atr_val):
if candle.State != CandleStates.Finished:
return
atr_value = float(atr_val)
if atr_value <= 0:
self._save_prev(candle)
return
if self._cooldown > 0:
self._cooldown -= 1
if self._has_prev and self._cooldown == 0 and float(self.Position) == 0:
prev_body = abs(self._prev_close - self._prev_open)
min_body = atr_value * float(self.BodyMinAtr)
if float(candle.ClosePrice) > float(candle.OpenPrice) and self._prev_close < self._prev_open and prev_body >= min_body:
self.BuyMarket()
self._cooldown = self.CooldownBars
elif float(candle.ClosePrice) < float(candle.OpenPrice) and self._prev_close > self._prev_open and prev_body >= min_body:
self.SellMarket()
self._cooldown = self.CooldownBars
self._save_prev(candle)
def _save_prev(self, candle):
self._prev_open = float(candle.OpenPrice)
self._prev_close = float(candle.ClosePrice)
self._has_prev = True
def OnReseted(self):
super(aeron_jjn_scalper_ea_strategy, self).OnReseted()
self._has_prev = False
self._cooldown = 0
self._prev_open = 0.0
self._prev_close = 0.0
def CreateClone(self):
return aeron_jjn_scalper_ea_strategy()