JB戦略
概要
JB戦略はfxDreemaのエキスパートアドバイザーを元にしており、長期トレンドフィルター、モメンタム確認、ボラティリティブレイクアウトを組み合わせます。
- トレンドフィルター: 直前のローソク足終値が100期間単純移動平均より上(ロング)または下(ショート)にあることを求めます。
- モメンタムフィルター: 100期間Force Indexで方向を確認します(ロングは正、ショートは負)。
- ボラティリティトリガー: 直前終値が対応するボリンジャーバンド(20期間、偏差2.0)を突き抜けたときにエントリーします。
- ポジション管理: 損失サイクル後はマーチンゲール風の乗数で注文数量を増やし、利益サイクル後は基本数量へ戻します。
- 決済ルール: 1契約あたりの平均含み益が設定した金額目標に達すると、すべてのオープンポジションを閉じます。
パラメーター
| 名前 | 説明 |
|---|---|
SmaPeriod |
SMAトレンドフィルターの長さ。既定値: 100。 |
ForcePeriod |
Force Index指標の長さ。既定値: 100。 |
BollingerPeriod |
ボリンジャーバンドの長さ。既定値: 20。 |
BollingerDeviation |
ボリンジャーバンドの標準偏差乗数。既定値: 2.0。 |
BaseVolume |
マーチンゲール調整前の初期注文数量。既定値: 0.1。 |
LossMultiplier |
損失サイクル後の次回注文数量に適用する乗数。既定値: 1.55。 |
AverageProfitTarget |
すべてのポジションを閉じるために必要な1契約あたりの平均含み益。既定値: 2.8。 |
CandleType |
計算に使うローソク足タイプ(既定は1分足)。 |
シグナル
ロングエントリー
- 直前ローソク足の終値が下側ボリンジャーバンド以下である。
- 直前終値が100期間SMAより高い(上向きトレンド)。
- Force Indexの値が正である。
ショートエントリー
- 直前ローソク足の終値が上側ボリンジャーバンド以上である。
- 直前終値が100期間SMAより低い(下向きトレンド)。
- Force Indexの値が負である。
決済
- すべてのオープンポジション全体で1契約あたりの平均含み益が
AverageProfitTargetに達すると、全ポジションを成行で閉じます。 - ポジションがフラットになるたびに、次回注文数量を調整します。損失サイクル後は
LossMultiplierを掛け、利益サイクル後はBaseVolumeへ戻します。
注記
- マーチンゲール調整は実現PnLを使って損失連続の発生を判断します。数量増加が許容される銘柄でのみ使用してください。
- StockSharp戦略はネットポジションで動作するため、MQL版のヘッジ(ロングとショートのバスケット同時保有)は集約ポジションで近似されます。
using System;
using System.Collections.Generic;
using System.Linq;
using Ecng.Common;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// JB: Bollinger breakout with long-term SMA, Force Index and martingale sizing.
/// </summary>
public class JbStrategy : Strategy
{
private readonly StrategyParam<int> _smaPeriod;
private readonly StrategyParam<int> _forcePeriod;
private readonly StrategyParam<int> _bollingerPeriod;
private readonly StrategyParam<decimal> _bollingerDeviation;
private readonly StrategyParam<decimal> _baseVolume;
private readonly StrategyParam<decimal> _lossMultiplier;
private readonly StrategyParam<decimal> _averageProfitTarget;
private readonly StrategyParam<DataType> _candleType;
private readonly List<decimal> _closes = [];
private decimal? _forceEma;
private decimal? _lastClose;
private decimal? _prevClose;
private decimal? _prevSma;
private decimal? _prevForce;
private decimal? _prevLower;
private decimal? _prevUpper;
private decimal _nextVolume;
private decimal _cycleRealizedStart;
private bool _cycleActive;
public int SmaPeriod { get => _smaPeriod.Value; set => _smaPeriod.Value = value; }
public int ForcePeriod { get => _forcePeriod.Value; set => _forcePeriod.Value = value; }
public int BollingerPeriod { get => _bollingerPeriod.Value; set => _bollingerPeriod.Value = value; }
public decimal BollingerDeviation { get => _bollingerDeviation.Value; set => _bollingerDeviation.Value = value; }
public decimal BaseVolume { get => _baseVolume.Value; set => _baseVolume.Value = value; }
public decimal LossMultiplier { get => _lossMultiplier.Value; set => _lossMultiplier.Value = value; }
public decimal AverageProfitTarget { get => _averageProfitTarget.Value; set => _averageProfitTarget.Value = value; }
public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
public JbStrategy()
{
_smaPeriod = Param(nameof(SmaPeriod), 100).SetGreaterThanZero();
_forcePeriod = Param(nameof(ForcePeriod), 100).SetGreaterThanZero();
_bollingerPeriod = Param(nameof(BollingerPeriod), 20).SetGreaterThanZero();
_bollingerDeviation = Param(nameof(BollingerDeviation), 2m).SetGreaterThanZero();
_baseVolume = Param(nameof(BaseVolume), 0.1m).SetGreaterThanZero();
_lossMultiplier = Param(nameof(LossMultiplier), 1.55m).SetGreaterThanZero();
_averageProfitTarget = Param(nameof(AverageProfitTarget), 2.8m).SetNotNegative();
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(1).TimeFrame());
}
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
=> [(Security, CandleType)];
protected override void OnReseted()
{
base.OnReseted();
_closes.Clear();
_forceEma = null;
_lastClose = null;
_prevClose = null;
_prevSma = null;
_prevForce = null;
_prevLower = null;
_prevUpper = null;
_nextVolume = BaseVolume;
_cycleRealizedStart = 0m;
_cycleActive = false;
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_nextVolume = NormalizeVolume(BaseVolume);
SubscribeCandles(CandleType).Bind(ProcessCandle).Start();
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
if (Position != 0m)
{
var perContract = Math.Abs(Position) > 0m
? PnLManager.UnrealizedPnL / Math.Abs(Position)
: 0m;
if (perContract >= AverageProfitTarget)
{
if (Position > 0m)
SellMarket(Math.Abs(Position));
else
BuyMarket(Math.Abs(Position));
UpdateIndicators(candle);
return;
}
}
else if (_prevClose is decimal prevClose &&
_prevSma is decimal prevSma &&
_prevForce is decimal prevForce &&
_prevLower is decimal prevLower &&
_prevUpper is decimal prevUpper)
{
var signal = GetSignal(prevClose, prevSma, prevForce, prevLower, prevUpper);
if (signal > 0)
Enter(Sides.Buy);
else if (signal < 0)
Enter(Sides.Sell);
}
UpdateIndicators(candle);
}
private void UpdateIndicators(ICandleMessage candle)
{
if (_lastClose is decimal last)
{
var rawForce = (candle.ClosePrice - last) * candle.TotalVolume;
_forceEma = Ema(_forceEma, rawForce, ForcePeriod);
}
_lastClose = candle.ClosePrice;
_closes.Add(candle.ClosePrice);
var keep = Math.Max(SmaPeriod, BollingerPeriod);
if (_closes.Count > keep)
_closes.RemoveRange(0, _closes.Count - keep);
if (_closes.Count < SmaPeriod || _closes.Count < BollingerPeriod || _forceEma is null)
return;
var sma = _closes.Skip(_closes.Count - SmaPeriod).Average();
var bb = _closes.Skip(_closes.Count - BollingerPeriod).ToArray();
var mean = bb.Average();
var variance = bb.Select(v => (v - mean) * (v - mean)).Average();
var std = (decimal)Math.Sqrt((double)variance);
_prevClose = candle.ClosePrice;
_prevSma = sma;
_prevForce = _forceEma.Value;
_prevLower = mean - BollingerDeviation * std;
_prevUpper = mean + BollingerDeviation * std;
}
private void Enter(Sides side)
{
var volume = NormalizeVolume(_nextVolume);
if (volume <= 0m)
return;
_cycleRealizedStart = PnLManager.RealizedPnL;
_cycleActive = true;
if (side == Sides.Buy)
BuyMarket(volume);
else
SellMarket(volume);
}
protected override void OnOwnTradeReceived(MyTrade trade)
{
base.OnOwnTradeReceived(trade);
if (!_cycleActive || Position != 0m)
return;
var cyclePnL = PnLManager.RealizedPnL - _cycleRealizedStart;
_nextVolume = cyclePnL < 0m
? NormalizeVolume(_nextVolume * LossMultiplier)
: NormalizeVolume(BaseVolume);
_cycleActive = false;
}
internal static int GetSignal(decimal previousClose, decimal sma, decimal force, decimal lowerBand, decimal upperBand)
{
if (previousClose <= lowerBand && previousClose > sma && force > 0m)
return 1;
if (previousClose >= upperBand && previousClose < sma && force < 0m)
return -1;
return 0;
}
private decimal NormalizeVolume(decimal volume)
{
if (Security?.MaxVolume is decimal max && max > 0m)
volume = Math.Min(volume, max);
if (Security?.MinVolume is decimal min && min > 0m)
volume = Math.Max(volume, min);
if (Security?.VolumeStep is decimal step && step > 0m)
volume = Math.Floor(volume / step) * step;
return volume;
}
private static decimal Ema(decimal? previous, decimal value, int period)
{
if (previous is null)
return value;
var alpha = 2m / (period + 1m);
return previous.Value + alpha * (value - previous.Value);
}
}
import clr
import math
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Math
from StockSharp.Messages import DataType, CandleStates, Sides
from StockSharp.Algo.Strategies import Strategy
class jb_strategy(Strategy):
def __init__(self):
super(jb_strategy, self).__init__()
self._sma_period = self.Param("SmaPeriod", 100).SetGreaterThanZero()
self._force_period = self.Param("ForcePeriod", 100).SetGreaterThanZero()
self._bollinger_period = self.Param("BollingerPeriod", 20).SetGreaterThanZero()
self._bollinger_deviation = self.Param("BollingerDeviation", 2.0).SetGreaterThanZero()
self._base_volume = self.Param("BaseVolume", 0.1).SetGreaterThanZero()
self._loss_multiplier = self.Param("LossMultiplier", 1.55).SetGreaterThanZero()
self._average_profit_target = self.Param("AverageProfitTarget", 2.8).SetNotNegative()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(1)))
self._closes = []
self._force_ema = None
self._last_close = None
self._prev_close = None
self._prev_sma = None
self._prev_force = None
self._prev_lower = None
self._prev_upper = None
self._next_volume = 0.1
self._cycle_realized_start = 0.0
self._cycle_active = False
def GetWorkingSecurities(self):
return [(self.Security, self._candle_type.Value)]
def OnReseted(self):
super(jb_strategy, self).OnReseted()
self._closes = []
self._force_ema = None
self._last_close = None
self._prev_close = None
self._prev_sma = None
self._prev_force = None
self._prev_lower = None
self._prev_upper = None
self._next_volume = float(self._base_volume.Value)
self._cycle_realized_start = 0.0
self._cycle_active = False
def OnStarted2(self, time):
super(jb_strategy, self).OnStarted2(time)
self._next_volume = self._normalize_volume(float(self._base_volume.Value))
self.SubscribeCandles(self._candle_type.Value).Bind(self._process_candle).Start()
def _process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
if self.Position != 0:
abs_position = abs(float(self.Position))
per_contract = float(self.PnLManager.UnrealizedPnL) / abs_position if abs_position > 0 else 0.0
if per_contract >= float(self._average_profit_target.Value):
if self.Position > 0:
self.SellMarket(Math.Abs(self.Position))
else:
self.BuyMarket(Math.Abs(self.Position))
self._update_indicators(candle)
return
elif all(v is not None for v in [self._prev_close, self._prev_sma, self._prev_force, self._prev_lower, self._prev_upper]):
signal = self.get_signal(self._prev_close, self._prev_sma, self._prev_force, self._prev_lower, self._prev_upper)
if signal > 0:
self._enter(Sides.Buy)
elif signal < 0:
self._enter(Sides.Sell)
self._update_indicators(candle)
def _update_indicators(self, candle):
close = float(candle.ClosePrice)
if self._last_close is not None:
raw_force = (close - self._last_close) * float(candle.TotalVolume)
self._force_ema = self._ema(self._force_ema, raw_force, int(self._force_period.Value))
self._last_close = close
self._closes.append(close)
keep = max(int(self._sma_period.Value), int(self._bollinger_period.Value))
if len(self._closes) > keep:
del self._closes[:-keep]
sma_period = int(self._sma_period.Value)
bb_period = int(self._bollinger_period.Value)
if len(self._closes) < sma_period or len(self._closes) < bb_period or self._force_ema is None:
return
sma = sum(self._closes[-sma_period:]) / float(sma_period)
bb = self._closes[-bb_period:]
mean = sum(bb) / float(bb_period)
variance = sum((v - mean) ** 2 for v in bb) / float(bb_period)
std = math.sqrt(variance)
self._prev_close = close
self._prev_sma = sma
self._prev_force = self._force_ema
self._prev_lower = mean - float(self._bollinger_deviation.Value) * std
self._prev_upper = mean + float(self._bollinger_deviation.Value) * std
def _enter(self, side):
volume = self._normalize_volume(self._next_volume)
if volume <= 0:
return
self._cycle_realized_start = float(self.PnLManager.RealizedPnL)
self._cycle_active = True
if side == Sides.Buy:
self.BuyMarket(volume)
else:
self.SellMarket(volume)
def OnOwnTradeReceived(self, trade):
super(jb_strategy, self).OnOwnTradeReceived(trade)
if not self._cycle_active or self.Position != 0:
return
cycle_pnl = float(self.PnLManager.RealizedPnL) - self._cycle_realized_start
if cycle_pnl < 0:
self._next_volume = self._normalize_volume(self._next_volume * float(self._loss_multiplier.Value))
else:
self._next_volume = self._normalize_volume(float(self._base_volume.Value))
self._cycle_active = False
@staticmethod
def get_signal(previous_close, sma, force, lower_band, upper_band):
if previous_close <= lower_band and previous_close > sma and force > 0:
return 1
if previous_close >= upper_band and previous_close < sma and force < 0:
return -1
return 0
def _normalize_volume(self, volume):
if self.Security is not None:
if self.Security.MaxVolume is not None and float(self.Security.MaxVolume) > 0:
volume = min(volume, float(self.Security.MaxVolume))
if self.Security.MinVolume is not None and float(self.Security.MinVolume) > 0:
volume = max(volume, float(self.Security.MinVolume))
if self.Security.VolumeStep is not None and float(self.Security.VolumeStep) > 0:
step = float(self.Security.VolumeStep)
volume = math.floor(volume / step) * step
return volume
@staticmethod
def _ema(previous, value, period):
if previous is None:
return value
alpha = 2.0 / (period + 1.0)
return previous + alpha * (value - previous)
def CreateClone(self):
return jb_strategy()