在 GitHub 上查看
Spasm 策略
概述
- 将 MetaTrader 5 专家顾问 Spasm (barabashkakvn's edition) 转换为 StockSharp 高级 API 实现。
- 依据最近的波动率构建自适应通道,在多头与空头状态之间切换并捕捉突破。
- 适用于
CandleType 参数指定的任意品种与周期,默认使用 1 小时K线。
数据准备流程
- 订阅由
CandleType 描述的策略标的烛线序列。
- 使用最近
VolatilityPeriod 根K线构建波动率估计值:
- 当
UseWeightedVolatility 关闭时,使用简单移动平均处理每根K线的波动范围。
- 当
UseWeightedVolatility 打开时,改用线性加权移动平均,提升最新样本的权重。
- 默认情况下波动范围等于
High - Low。若启用 UseOpenCloseRange,则改用开盘价与收盘价的绝对差值,以对齐原始EA的模式切换。
- 将平均范围转换为最小价格跳动数并乘以
VolatilityMultiplier,向下取整后再乘回最小跳动,得到最终的突破阈值。
- 在前
VolatilityPeriod * 3 根已完成K线中记录最近的最高点与最低点及其时间戳,用于确定初始的趋势方向与参考价格。
参数
| 名称 |
默认值 |
说明 |
Volume |
1 |
每次进场使用的下单量。 |
VolatilityMultiplier |
5 |
乘以平均波动率后得到突破缓冲区的倍率。 |
VolatilityPeriod |
24 |
波动率计算与初始摆动扫描所需的K线数量。 |
UseWeightedVolatility |
false |
将波动率均值从简单移动平均切换为线性加权移动平均。 |
UseOpenCloseRange |
false |
使用开收盘价差作为波动来源,替代最高价减最低价。 |
StopLossFraction |
0.5 |
以波动率阈值的比例计算止损距离,最小执行距离为三个最小跳动。 |
CandleType |
1 小时时间框架 |
所有计算使用的K线类型与周期。 |
交易逻辑
- 趋势跟踪
_highestPrice 与 _lowestPrice 保存当前摆动的锚点。
- 当价格上破
_highestPrice + threshold 时,将 _highestPrice 更新为当根K线的最高价;当价格下破 _lowestPrice - threshold 时,将 _lowestPrice 更新为当根K线的最低价。
- 布尔变量
_isTrendUp 表示当前处于多头(true)或空头(false)状态。
- 入场规则
- 当
_isTrendUp 为 false 且收盘价高于 _lowestPrice + threshold,趋势翻转为多头,执行 BuyMarket(Volume + Math.Abs(Position)),平掉所有空单并开出指定数量的多单。
- 当
_isTrendUp 为 true 且收盘价低于 _highestPrice - threshold,趋势翻转为空头,执行 SellMarket(Volume + Math.Abs(Position)),完成反向操作。
- 止损管理
- 建立多头后,将止损设置在
entry - max(threshold * StopLossFraction, 3 * priceStep)。
- 建立空头后,将止损设置在
entry + max(threshold * StopLossFraction, 3 * priceStep)。
- 若某根K线的最低价触及多头止损或最高价触及空头止损,则通过市价单退出对应持仓。当
StopLossFraction 为零时不启用止损。
- 风险控制与架构
- 在启动阶段调用
StartProtection(),使平台自带的风控组件立即生效。
- 仅处理收盘完成的K线,避免分时噪声并与原EA的逐K更新方式保持一致。
- 注释与参数名称全部使用英文,满足项目要求。
与 MQL 版本的差异
- 原始EA在每个tick上重新计算阈值。本策略在完成的K线上执行相同逻辑,因为高级API基于烛线订阅运行。
- 止损触发基于K线数据评估,因此盘中触发后同根K线内的反向变化会在收盘时处理。
- StockSharp 中无法直接获取与 MetaTrader 相同的点差与最小止损距离,因此在计算出的止损过小时采用三个最小跳动作为保守下限,以模拟原实现的回退机制。
使用说明
- 确认标的提供有效的
PriceStep 值;若未提供,将默认使用 1 作为最小跳动。
- 策略方向中性,可用于现货、期货或差价合约,只要行情源提供指定的K线数据即可。
- 策略没有预设止盈目标,退出完全依赖趋势翻转或止损触发。
namespace StockSharp.Samples.Strategies;
using System;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.Messages;
/// <summary>
/// Volatility breakout strategy converted from the MetaTrader Spasm expert advisor.
/// Tracks directional swings using adaptive thresholds derived from ATR.
/// Buys when price breaks above recent high + ATR*multiplier, sells when price breaks below recent low - ATR*multiplier.
/// </summary>
public class SpasmStrategy : Strategy
{
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<decimal> _volatilityMultiplier;
private decimal _highestPrice;
private decimal _lowestPrice;
private decimal _prevRange;
private bool _initialized;
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
public decimal VolatilityMultiplier
{
get => _volatilityMultiplier.Value;
set => _volatilityMultiplier.Value = value;
}
public SpasmStrategy()
{
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
.SetDisplay("Candle Type", "Timeframe for analysis", "General");
_volatilityMultiplier = Param(nameof(VolatilityMultiplier), 2m)
.SetGreaterThanZero()
.SetDisplay("Volatility Multiplier", "Multiplier applied to ATR for breakout bands", "Trading");
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_highestPrice = 0m;
_lowestPrice = decimal.MaxValue;
_prevRange = 0m;
_initialized = false;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_highestPrice = 0m;
_lowestPrice = decimal.MaxValue;
_prevRange = 0m;
_initialized = false;
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(OnProcess)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawOwnTrades(area);
}
}
private void OnProcess(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
if (!IsFormedAndOnlineAndAllowTrading())
return;
if (!_initialized)
{
_highestPrice = candle.HighPrice;
_lowestPrice = candle.LowPrice;
_prevRange = candle.HighPrice - candle.LowPrice;
_initialized = true;
return;
}
// Update extremes
if (candle.HighPrice > _highestPrice)
_highestPrice = candle.HighPrice;
if (candle.LowPrice < _lowestPrice)
_lowestPrice = candle.LowPrice;
var threshold = _prevRange * VolatilityMultiplier;
if (threshold <= 0)
return;
// Breakout above lowest + threshold => buy
if (candle.ClosePrice > _lowestPrice + threshold && Position <= 0)
{
BuyMarket();
// Reset extremes after entry
_highestPrice = candle.HighPrice;
_lowestPrice = candle.LowPrice;
}
// Breakout below highest - threshold => sell
else if (candle.ClosePrice < _highestPrice - threshold && Position >= 0)
{
SellMarket();
// Reset extremes after entry
_highestPrice = candle.HighPrice;
_lowestPrice = candle.LowPrice;
}
_prevRange = candle.HighPrice - candle.LowPrice;
}
}
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
class spasm_strategy(Strategy):
def __init__(self):
super(spasm_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4))) \
.SetDisplay("Candle Type", "Timeframe for analysis", "General")
self._volatility_multiplier = self.Param("VolatilityMultiplier", 2.0) \
.SetDisplay("Volatility Multiplier", "Multiplier applied to ATR for breakout bands", "Trading")
self._highest_price = 0.0
self._lowest_price = float('inf')
self._prev_range = 0.0
self._initialized = False
@property
def CandleType(self):
return self._candle_type.Value
@property
def VolatilityMultiplier(self):
return self._volatility_multiplier.Value
def OnReseted(self):
super(spasm_strategy, self).OnReseted()
self._highest_price = 0.0
self._lowest_price = float('inf')
self._prev_range = 0.0
self._initialized = False
def OnStarted2(self, time):
super(spasm_strategy, self).OnStarted2(time)
self._highest_price = 0.0
self._lowest_price = float('inf')
self._prev_range = 0.0
self._initialized = False
subscription = self.SubscribeCandles(self.CandleType)
subscription \
.Bind(self._on_process) \
.Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawOwnTrades(area)
def _on_process(self, candle):
if candle.State != CandleStates.Finished:
return
high = float(candle.HighPrice)
low = float(candle.LowPrice)
close = float(candle.ClosePrice)
if not self._initialized:
self._highest_price = high
self._lowest_price = low
self._prev_range = high - low
self._initialized = True
return
if high > self._highest_price:
self._highest_price = high
if low < self._lowest_price:
self._lowest_price = low
threshold = self._prev_range * float(self.VolatilityMultiplier)
if threshold <= 0:
self._prev_range = high - low
return
if close > self._lowest_price + threshold and self.Position <= 0:
self.BuyMarket()
self._highest_price = high
self._lowest_price = low
elif close < self._highest_price - threshold and self.Position >= 0:
self.SellMarket()
self._highest_price = high
self._lowest_price = low
self._prev_range = high - low
def CreateClone(self):
return spasm_strategy()