トレンドスキャルパー戦略 (API/3858)
概要
TrendScalperStrategy は、MetaTrader 4 エキスパート アドバイザー Currencyprofits_01_1.mq4 を C# に変換したものです。オリジナルのロボットは、短期的な EMA/SMA クロスオーバー フィルターと、直近のスイング高値と安値付近のブレイクアウト エントリーを組み合わせた軽量のトレンド追跡スキャルパーです。 StockSharp ポートは、フレームワークの高レベルのローソク足サブスクリプションとインジケーター パイプラインを採用しながら、同じ決定ルールを維持します。
取引ロジック
- 指標
- 終値では高速 EMA (デフォルト 6)。
- 終値では遅い SMA (デフォルトは 12)。
- ろうそくの高値と安値から計算された最高値 (デフォルトのウィンドウ 6) と最低値 (デフォルトのウィンドウ 6)。
- エントリー条件
- ロング: 価格は最近のローバンド (
Lowest Low) に突入しますが、高速の EMA は低速の SMA を上回ります。この戦略は、資金管理ルールで定義された数量で成行買い注文を送信します。
- ショート: 価格は最近の高バンド (
Highest High) に接していますが、高速の EMA は低速の SMA を下回っています。成行売り注文は、同じ数量計算を使用して発注されます。
- ポジションがオープンしている間、システムはフラットを維持し、MQL バージョンの単一注文の動作を反映しています。
- Exit Conditions
- ロングイグジット: オープンしているロングポジションがローソク足の高値で記録された
Highest High を突破すると、そのポジションは市場でクローズされます。
- ショートエグジット: オープンショートポジションがローソク足の安値が
Lowest Low を通過するのを観察すると、ショートは市場でカバーされます。
StopLossPoints がゼロより大きい場合、StartProtection によって管理される保護ストップロスがすべての取引に適用されます。
資金管理
ロットサイジング ロジックは、MQL スクリプトで公開されている 3 つのモードを再現します。
| モード |
説明 |
港内での行動 |
0 |
固定ロット (LotsIfNoMM)。 |
構成された FixedVolume を返します。 |
<0 |
口座残高とリスクファクターから計算された端数ロット。 |
ceil(balance * risk / 10000) / 10 を計算します。上限は 100 ロットです。 |
>0 |
バランスとリスクファクターからのホールロットスケーリング。 |
同じ基本式を使用しますが、結果は次の整数に切り上げられ、1 ロットで下限され、100 で上限が設定されます。 |
残高は Portfolio.CurrentValue から取得されます (BeginValue にフォールバックします)。ポートフォリオの値が利用できない場合、戦略は固定量に戻るため、バックテスト中も注文は発行されます。
リスク管理
- ストップロス:
StopLossPoints パラメータは価格ポイント (pips) で表されます。 OnStarted の間、距離は Security.PriceStep 倍されて StartProtection に渡され、StockSharp は保護命令を維持できます。
- 単一ポジション: ロジックは新しい取引を開始する前に
Position == 0 を強制し、MT4 エキスパートとまったく同様にポジションの重複を防ぎます。
パラメーター
| 名前 |
デフォルト |
説明 |
CandleType |
15分の時間枠 |
インジケーターの計算とシグナルに使用されるローソク足シリーズ。 |
FastLength |
6 |
高速の EMA の期間。 |
SlowLength |
12 |
遅い SMA の期間。 |
BreakoutWindow |
6 |
最高高/最低低ブレークアウト フィルターについて検査されたキャンドルの数。 |
FixedVolume |
0.1ロット |
資金管理が無効になっている場合、またはフォールバックが必要な場合のボリューム。 |
MoneyManagementMode |
0 |
ロットサイジングを固定、分数、または丸めから選択します。 |
MoneyManagementRisk |
40 |
バランスベースのロットサイジングで使用されるリスク係数乗数。 |
StopLossPoints |
50 |
価格ポイントでのストップロス距離 (StartProtection を呼び出す前に絶対価格に変換)。 |
実装メモ
- インジケーター チェーンは高レベルの
SubscribeCandles().Bind(...) ワークフローに依存します。手動によるシリーズバッファリングは必要ありません。
- コード内のコメントは、リポジトリのガイドラインに一致するように英語で追加されました。
- 単体テストは変更されていません。この変換の焦点は、戦略とそれに付随するドキュメントです。
使用のヒント
- 元の取引環境(スキャルピングの日中の短い時間枠など)に一致するローソク足の間隔を選択します。
- 絶対価格へのストップロス変換が正しく機能するように、ポートフォリオに有効な
PriceStep があることを確認してください。
MoneyManagementRisk は慎重に調整してください。MQL のエキスパートから継承された ceil(balance * risk / 10000) の計算により、値が大きいほどポジションが大きくなります。
厳密なシグナル条件: チャネル高値・安値はシグナル足を除く 直前の確定済み BreakoutWindow 本で計算します。買いは EMA(FastLength) > SMA(SlowLength) かつ currentLow <= previousWindowLow、売りは逆のトレンドかつ currentHigh >= previousWindowHigh が必要です。ロングは currentHigh > previousWindowHigh、ショートは currentLow < previousWindowLow で決済します。両移動平均とチャネルのウォームアップ完了前は取引しません。StopLossPoints > 0 の場合、StartProtection で保護ストップを有効化します。
namespace StockSharp.Samples.Strategies;
using System;
using System.Collections.Generic;
using System.Linq;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
/// <summary>
/// Fast EMA versus slow SMA direction filtered by a sweep of the previous
/// candle-high/low band. Independent natural exits and a native stop protect fills.
/// </summary>
public class TrendScalperStrategy : Strategy
{
private readonly StrategyParam<int> _fastLength;
private readonly StrategyParam<int> _slowLength;
private readonly StrategyParam<int> _breakoutWindow;
private readonly StrategyParam<decimal> _fixedVolume;
private readonly StrategyParam<int> _moneyManagementMode;
private readonly StrategyParam<decimal> _moneyManagementRisk;
private readonly StrategyParam<decimal> _stopLossPoints;
private readonly StrategyParam<DataType> _candleType;
private readonly Queue<decimal> _fastSeed = new();
private readonly Queue<decimal> _slowCloses = new();
private readonly Queue<decimal> _highs = new();
private readonly Queue<decimal> _lows = new();
private decimal? _fastEma;
private Order _pendingOrder;
public int FastLength { get => _fastLength.Value; set => _fastLength.Value = value; }
public int SlowLength { get => _slowLength.Value; set => _slowLength.Value = value; }
public int BreakoutWindow { get => _breakoutWindow.Value; set => _breakoutWindow.Value = value; }
public decimal FixedVolume { get => _fixedVolume.Value; set => _fixedVolume.Value = value; }
public int MoneyManagementMode { get => _moneyManagementMode.Value; set => _moneyManagementMode.Value = value; }
public decimal MoneyManagementRisk { get => _moneyManagementRisk.Value; set => _moneyManagementRisk.Value = value; }
public decimal StopLossPoints { get => _stopLossPoints.Value; set => _stopLossPoints.Value = value; }
public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
public TrendScalperStrategy()
{
_fastLength = Param(nameof(FastLength), 6).SetGreaterThanZero();
_slowLength = Param(nameof(SlowLength), 12).SetGreaterThanZero();
_breakoutWindow = Param(nameof(BreakoutWindow), 6).SetGreaterThanZero();
_fixedVolume = Param(nameof(FixedVolume), 0.1m).SetGreaterThanZero();
_moneyManagementMode = Param(nameof(MoneyManagementMode), 0);
_moneyManagementRisk = Param(nameof(MoneyManagementRisk), 40m).SetNotNegative();
_stopLossPoints = Param(nameof(StopLossPoints), 50m).SetNotNegative();
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(15).TimeFrame());
}
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
=> [(Security, CandleType), (Security, DataType.Level1)];
protected override void OnReseted()
{
base.OnReseted();
ClearState();
}
private void ClearState()
{
_fastSeed.Clear();
_slowCloses.Clear();
_highs.Clear();
_lows.Clear();
_fastEma = null;
_pendingOrder = null;
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
ClearState();
var step = Security?.PriceStep ?? 0m;
if (step <= 0m)
throw new InvalidOperationException("Trend Scalper requires a positive PriceStep.");
if (StopLossPoints > 0m)
{
StartProtection(new Unit(), new Unit(StopLossPoints * step, UnitTypes.Absolute),
useMarketOrders: true, isLocalStop: true);
foreach (var field in new[] { Level1Fields.BestBidPrice, Level1Fields.BestAskPrice })
{
var quotes = new Subscription(DataType.Level1, Security);
quotes.MarketData.BuildField = field;
SubscribeLevel1(quotes).Bind(ObserveQuote).Start();
}
}
SubscribeCandles(CandleType).Bind(ProcessCandle).Start();
}
private void ObserveQuote(Level1ChangeMessage message)
{
// A Level1 stream is needed for native stop processing between candle closes.
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
var hasBand = _highs.Count == BreakoutWindow;
var bandHigh = hasBand ? _highs.Max() : 0m;
var bandLow = hasBand ? _lows.Min() : 0m;
_highs.Enqueue(candle.HighPrice);
_lows.Enqueue(candle.LowPrice);
while (_highs.Count > BreakoutWindow) _highs.Dequeue();
while (_lows.Count > BreakoutWindow) _lows.Dequeue();
var price = candle.ClosePrice;
if (_fastEma is null)
{
_fastSeed.Enqueue(price);
if (_fastSeed.Count == FastLength)
_fastEma = _fastSeed.Average();
}
else
_fastEma += (price - _fastEma.Value) * 2m / (FastLength + 1);
_slowCloses.Enqueue(price);
while (_slowCloses.Count > SlowLength)
_slowCloses.Dequeue();
if (!hasBand || _fastEma is null || _slowCloses.Count < SlowLength ||
_pendingOrder is not null && _pendingOrder.State is not (OrderStates.Done or OrderStates.Failed))
return;
if (Position > 0m && candle.HighPrice > bandHigh)
{
Submit(Sides.Sell, Position, "Trend scalper band exit");
return;
}
if (Position < 0m && candle.LowPrice < bandLow)
{
Submit(Sides.Buy, Math.Abs(Position), "Trend scalper band exit");
return;
}
if (Position != 0m)
return;
var slow = _slowCloses.Average();
if (_fastEma > slow && candle.LowPrice <= bandLow)
Enter(Sides.Buy);
else if (_fastEma < slow && candle.HighPrice >= bandHigh)
Enter(Sides.Sell);
}
private void Enter(Sides side)
{
var volume = CalculateOrderVolume();
if (volume > 0m)
Submit(side, volume, "Trend scalper entry");
}
private decimal CalculateOrderVolume()
{
var volume = FixedVolume;
if (MoneyManagementMode != 0)
{
var balance = Portfolio?.CurrentValue ?? Portfolio?.BeginValue ?? 0m;
if (balance > 0m)
{
var fractional = Math.Ceiling(balance * MoneyManagementRisk / 10000m) / 10m;
volume = MoneyManagementMode > 0 ? Math.Max(1m, Math.Ceiling(fractional)) : fractional;
volume = Math.Min(volume, 100m);
}
}
if (Security?.VolumeStep is decimal step && step > 0m)
volume = decimal.Floor(volume / step) * step;
if (Security?.MinVolume is decimal min && min > 0m)
volume = Math.Max(volume, min);
if (Security?.MaxVolume is decimal max && max > 0m)
volume = Math.Min(volume, max);
return volume;
}
private void Submit(Sides side, decimal volume, string comment)
{
_pendingOrder = new Order
{
Security = Security,
Portfolio = Portfolio,
Type = OrderTypes.Market,
Side = side,
Volume = volume,
Comment = comment,
};
RegisterOrder(_pendingOrder);
}
}
import clr
import math
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Strategies")
clr.AddReference("StockSharp.BusinessEntities")
from System import TimeSpan, Decimal
from StockSharp.Messages import (
DataType, CandleStates, Level1Fields, OrderTypes, OrderStates, Sides,
Unit, UnitTypes,
)
from StockSharp.BusinessEntities import Order, Subscription
from StockSharp.Algo.Strategies import Strategy
class trend_scalper_strategy(Strategy):
"""Fast close EMA, slow close SMA, and previous-bar high/low breakout bands."""
def __init__(self):
super(trend_scalper_strategy, self).__init__()
self._fast_length = self.Param("FastLength", 6).SetGreaterThanZero()
self._slow_length = self.Param("SlowLength", 12).SetGreaterThanZero()
self._breakout_window = self.Param("BreakoutWindow", 6).SetGreaterThanZero()
self._fixed_volume = self.Param("FixedVolume", 0.1).SetGreaterThanZero()
self._money_management_mode = self.Param("MoneyManagementMode", 0)
self._money_management_risk = self.Param("MoneyManagementRisk", 40.0).SetNotNegative()
self._stop_loss_points = self.Param("StopLossPoints", 50.0).SetNotNegative()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(15)))
self._clear_state()
@property
def candle_type(self):
return self._candle_type.Value
def _clear_state(self):
self._fast_seed = []
self._slow_closes = []
self._highs = []
self._lows = []
self._fast_ema = None
self._pending_order = None
def OnReseted(self):
super(trend_scalper_strategy, self).OnReseted()
self._clear_state()
def GetWorkingSecurities(self):
return [(self.Security, self.candle_type), (self.Security, DataType.Level1)]
def OnStarted2(self, time):
super(trend_scalper_strategy, self).OnStarted2(time)
self._clear_state()
step = float(self.Security.PriceStep or 0)
if step <= 0:
raise ValueError("Trend Scalper requires a positive PriceStep.")
if self._stop_loss_points.Value > 0:
self.StartProtection(
Unit(), Unit(Decimal(float(self._stop_loss_points.Value) * step), UnitTypes.Absolute),
useMarketOrders=True, isLocalStop=True)
for field in (Level1Fields.BestBidPrice, Level1Fields.BestAskPrice):
quotes = Subscription(DataType.Level1, self.Security)
quotes.MarketData.BuildField = field
self.SubscribeLevel1(quotes).Bind(self._observe_quote).Start()
self.SubscribeCandles(self.candle_type).Bind(self._process_candle).Start()
def _observe_quote(self, message):
# Keeps native protection informed between completed candles.
pass
def _process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
has_band = len(self._highs) == self._breakout_window.Value
band_high = max(self._highs) if has_band else 0.0
band_low = min(self._lows) if has_band else 0.0
self._highs.append(float(candle.HighPrice))
self._lows.append(float(candle.LowPrice))
if len(self._highs) > self._breakout_window.Value:
self._highs.pop(0)
self._lows.pop(0)
close = float(candle.ClosePrice)
fast_length = self._fast_length.Value
if self._fast_ema is None:
self._fast_seed.append(close)
if len(self._fast_seed) == fast_length:
self._fast_ema = sum(self._fast_seed) / fast_length
else:
self._fast_ema += (close - self._fast_ema) * 2.0 / (fast_length + 1)
self._slow_closes.append(close)
if len(self._slow_closes) > self._slow_length.Value:
self._slow_closes.pop(0)
if not has_band or self._fast_ema is None or len(self._slow_closes) < self._slow_length.Value:
return
if (self._pending_order is not None and
self._pending_order.State not in (OrderStates.Done, OrderStates.Failed)):
return
if self.Position > 0 and float(candle.HighPrice) > band_high:
self._submit(Sides.Sell, float(self.Position), "Trend scalper band exit")
return
if self.Position < 0 and float(candle.LowPrice) < band_low:
self._submit(Sides.Buy, abs(float(self.Position)), "Trend scalper band exit")
return
if self.Position != 0:
return
slow = sum(self._slow_closes) / len(self._slow_closes)
if self._fast_ema > slow and float(candle.LowPrice) <= band_low:
self._enter(Sides.Buy)
elif self._fast_ema < slow and float(candle.HighPrice) >= band_high:
self._enter(Sides.Sell)
def _enter(self, side):
volume = self._calculate_volume()
if volume > 0:
self._submit(side, volume, "Trend scalper entry")
def _calculate_volume(self):
volume = float(self._fixed_volume.Value)
mode = self._money_management_mode.Value
if mode != 0:
balance = (self.Portfolio.CurrentValue or self.Portfolio.BeginValue
if self.Portfolio is not None else 0)
balance = float(balance or 0)
if balance > 0:
fraction = math.ceil(balance * float(self._money_management_risk.Value) / 10000.0) / 10.0
volume = min(100.0, max(1.0, math.ceil(fraction)) if mode > 0 else fraction)
step = float(self.Security.VolumeStep or 0)
if step > 0:
volume = math.floor(volume / step) * step
minimum = float(self.Security.MinVolume or 0)
maximum = float(self.Security.MaxVolume or 0)
if minimum > 0:
volume = max(volume, minimum)
if maximum > 0:
volume = min(volume, maximum)
return volume
def _submit(self, side, volume, comment):
order = Order()
order.Security = self.Security
order.Portfolio = self.Portfolio
order.Type = OrderTypes.Market
order.Side = side
order.Volume = Decimal(volume)
order.Comment = comment
self._pending_order = order
self.RegisterOrder(order)
def CreateClone(self):
return trend_scalper_strategy()