在 GitHub 上查看
Backbone 篮子策略(StockSharp)
概述
Backbone 篮子策略 将 MetaTrader 4 的 "Backbone.mq4" 专家顾问迁移到 StockSharp 高层 API。系统先记录买卖价的极值以推断初始方向,然后在后续的每根已完成K线中逐步累积同向仓位。每根K线最多追加一笔市价单,直到达到 MaxTrades 限制,或是由止损/止盈订单结束整组仓位。仓位规模通过分数风险模型控制——以账户价值与止损距离计算可承受风险。
市场数据流程
- K线(
CandleType):只有在收到完整的K线后才会评估信号,与原始 EA 仅在 Bars > PrevBars 时触发完全一致。
- 盘口快照:持续跟踪最优买价与卖价,用于复制最初的极值判断以及移动止损的计算。
- 策略内部状态:StockSharp
Strategy 基类维护实时仓位、均价和盈亏,这些信息直接驱动保护性订单的更新。
交易逻辑
- 初始校准:在尚未确定方向时,策略会记录出现过的最高买价与最低卖价。一旦价格距离极值回撤达到
TrailingStopPoints * PriceStep,便选定第一组仓位的方向。
- 下单节奏:
- 若上一笔成交为做空(
_lastPositionDirection == -1)且当前没有持仓,则提交新的买入市价单。
- 若上一笔成交为做多(
_lastPositionDirection == 1)且尚未达到 MaxTrades 上限,则在后续 K 线上继续追加买单。
- 做空规则完全对称:当上一笔成交为做多且尚未达到上限时追加卖单。
- 仓位规模:每次下单都会调用 MQL 中
Vol() 的 C# 版本。策略取账户价值(按 CurrentValue → CurrentBalance → BeginBalance 的优先级)乘以 MaxRisk,再除以折算成货币单位的止损距离(利用 PriceStepCost)。结果对齐至 VolumeStep,并受 MinVolume 与 MaxVolume 约束,低于最小交易量时直接放弃下单。
- 保护性订单:成交后立即创建一个覆盖整组仓位的止损与止盈订单,距离以“点”(价格步长)表示,保持与原始 EA 一致。
- 移动止损:当
StopLossPoints 与 TrailingStopPoints 均大于零时,若价格超出入场价超过拖尾距离,则重新注册止损订单以锁定利润。多头使用最优买价作为参考,空头使用最优卖价。
- 仓位闭合:一旦止损或止盈触发,内部计数器清零,但
_lastPositionDirection 保留,使得下一根K线会在相反方向开启新一组仓位,从而复现原策略的交替行为。
资金管理
- 使用与 MQL 相同的分数公式
1 / (MaxTrades / MaxRisk - openTrades) 计算风险占比。
- 风险基数依次取
Portfolio.CurrentValue、CurrentBalance、BeginBalance。
- 调整后的仓位若低于
MinVolume 会被丢弃,确保遵守交易所最小手数。
- 每次仓位变化都会重新生成止损/止盈订单,使保护范围始终覆盖全部仓位。
参数
| 参数 |
默认值 |
说明 |
CandleType |
15 分钟 |
触发策略评估的 K 线周期。 |
MaxRisk |
0.5 |
单笔交易可使用的账户资金比例,必须为正。 |
MaxTrades |
10 |
同方向最多允许累积的市价单数量。 |
TakeProfitPoints |
170 |
止盈距离(以价格步长表示),设为 0 可关闭止盈。 |
StopLossPoints |
40 |
止损距离(以价格步长表示),也是风险计算和拖尾的基础。 |
TrailingStopPoints |
300 |
移动止损距离(以价格步长表示),设为 0 表示保持静态止损。 |
移植说明
- 原 EA 为每笔订单分别调整止损/止盈;StockSharp 版本在净持仓模式下改为为整组仓位维护一个聚合的保护订单。
- 仓位规模依赖
Security.PriceStepCost。若连接器未提供该值,则退回到策略的 Volume 属性。
- 移动止损仅在新K线完成时更新,保持与 MT4 中“每根K线执行一次”的行为一致。
_lastPositionDirection 保存上一笔成交方向,使得平仓后下一根K线自动在相反方向重新建仓。
- 本目录仅包含 C# 实现,不提供 Python 版本。
使用建议
- 选择具有正确
PriceStep、PriceStepCost 与成交量步长信息的品种,以确保仓位规模合理。
- 回测时需确保有盘口数据源,移动止损需要最优买卖价才能精确工作。
- 若想降低加仓速度,可提高
MaxTrades 或降低 MaxRisk,从而减小 Vol() 计算出的下单量。
using System;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Backbone strategy that alternates between long and short based on retracements from recent extremes.
/// Uses ATR to detect when price has pulled back enough from its high/low to enter a new position.
/// </summary>
public class BackboneBasketStrategy : Strategy
{
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _atrPeriod;
private readonly StrategyParam<decimal> _retraceMultiplier;
private decimal _highestPrice;
private decimal _lowestPrice;
private decimal _entryPrice;
private int _lastDirection; // 1 = long, -1 = short, 0 = none
public BackboneBasketStrategy()
{
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Timeframe used for analysis.", "General");
_atrPeriod = Param(nameof(AtrPeriod), 14)
.SetDisplay("ATR Period", "Period for ATR indicator.", "Indicators");
_retraceMultiplier = Param(nameof(RetraceMultiplier), 2m)
.SetDisplay("Retrace Multiplier", "ATR multiplier for retracement threshold.", "Signals");
}
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
public int AtrPeriod
{
get => _atrPeriod.Value;
set => _atrPeriod.Value = value;
}
public decimal RetraceMultiplier
{
get => _retraceMultiplier.Value;
set => _retraceMultiplier.Value = value;
}
/// <inheritdoc />
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_highestPrice = 0;
_lowestPrice = decimal.MaxValue;
_entryPrice = 0;
_lastDirection = 0;
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_highestPrice = 0;
_lowestPrice = decimal.MaxValue;
_entryPrice = 0;
_lastDirection = 0;
var atr = new AverageTrueRange { Length = AtrPeriod };
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(atr, ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, atr);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle, decimal atrValue)
{
if (candle.State != CandleStates.Finished)
return;
if (atrValue <= 0)
return;
var close = candle.ClosePrice;
// Track extremes
if (close > _highestPrice)
_highestPrice = close;
if (close < _lowestPrice)
_lowestPrice = close;
var threshold = atrValue * RetraceMultiplier;
// Exit existing positions on retracement
if (Position > 0 && close < _highestPrice - threshold)
{
SellMarket();
_entryPrice = 0;
_lastDirection = 1;
_lowestPrice = close;
}
else if (Position < 0 && close > _lowestPrice + threshold)
{
BuyMarket();
_entryPrice = 0;
_lastDirection = -1;
_highestPrice = close;
}
// Entry logic - alternate direction
if (Position == 0)
{
if (_lastDirection != 1 && close < _highestPrice - threshold)
{
// Price pulled back from high - sell
SellMarket();
_entryPrice = close;
_lastDirection = -1;
_lowestPrice = close;
}
else if (_lastDirection != -1 && close > _lowestPrice + threshold)
{
// Price bounced from low - buy
BuyMarket();
_entryPrice = close;
_lastDirection = 1;
_highestPrice = close;
}
}
}
}
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 AverageTrueRange
from StockSharp.Algo.Strategies import Strategy
class backbone_basket_strategy(Strategy):
def __init__(self):
super(backbone_basket_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))) \
.SetDisplay("Candle Type", "Timeframe used for analysis.", "General")
self._atr_period = self.Param("AtrPeriod", 14) \
.SetDisplay("ATR Period", "Period for ATR indicator.", "Indicators")
self._retrace_multiplier = self.Param("RetraceMultiplier", 2.0) \
.SetDisplay("Retrace Multiplier", "ATR multiplier for retracement threshold.", "Signals")
self._highest_price = 0.0
self._lowest_price = float('inf')
self._entry_price = 0.0
self._last_direction = 0
@property
def candle_type(self):
return self._candle_type.Value
@candle_type.setter
def candle_type(self, value):
self._candle_type.Value = value
@property
def atr_period(self):
return self._atr_period.Value
@atr_period.setter
def atr_period(self, value):
self._atr_period.Value = value
@property
def retrace_multiplier(self):
return self._retrace_multiplier.Value
@retrace_multiplier.setter
def retrace_multiplier(self, value):
self._retrace_multiplier.Value = value
def OnReseted(self):
super(backbone_basket_strategy, self).OnReseted()
self._highest_price = 0.0
self._lowest_price = float('inf')
self._entry_price = 0.0
self._last_direction = 0
def OnStarted2(self, time):
super(backbone_basket_strategy, self).OnStarted2(time)
self._highest_price = 0.0
self._lowest_price = float('inf')
self._entry_price = 0.0
self._last_direction = 0
atr = AverageTrueRange()
atr.Length = self.atr_period
subscription = self.SubscribeCandles(self.candle_type)
subscription.Bind(atr, self.OnProcess).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, atr)
self.DrawOwnTrades(area)
def OnProcess(self, candle, atr_value):
if candle.State != CandleStates.Finished:
return
if atr_value <= 0:
return
close = float(candle.ClosePrice)
if close > self._highest_price:
self._highest_price = close
if close < self._lowest_price:
self._lowest_price = close
threshold = float(atr_value) * self.retrace_multiplier
# Exit existing positions on retracement
if self.Position > 0 and close < self._highest_price - threshold:
self.SellMarket()
self._entry_price = 0.0
self._last_direction = 1
self._lowest_price = close
elif self.Position < 0 and close > self._lowest_price + threshold:
self.BuyMarket()
self._entry_price = 0.0
self._last_direction = -1
self._highest_price = close
# Entry logic - alternate direction
if self.Position == 0:
if self._last_direction != 1 and close < self._highest_price - threshold:
self.SellMarket()
self._entry_price = close
self._last_direction = -1
self._lowest_price = close
elif self._last_direction != -1 and close > self._lowest_price + threshold:
self.BuyMarket()
self._entry_price = close
self._last_direction = 1
self._highest_price = close
def CreateClone(self):
return backbone_basket_strategy()