Semilong WWW Forex Instruments Info 策略
概述
该策略复刻了 MetaTrader 专家顾问 “Semilong” 的交易逻辑。它同时观测当前买价与两个历史收盘价之间的距离,这两个收盘价之间相隔可配置的 K 线数量。当当前价格足够大幅度地低于(或高于)较新的参考收盘价,并且该收盘价又明显脱离更久远的收盘价时,策略会开仓做多或做空。头寸管理完全参照原始脚本,支持自定义止盈、止损、可选的移动止损,以及在连续亏损后自动降低仓位的自动手数模块。
信号生成
- 历史偏移:
ShiftOne决定第一个比较收盘价向前追溯多少根已完成的 K 线,ShiftTwo在此基础上再增加额外的偏移量以获得第二个收盘价。 - 偏离阈值:
MoveOnePoints规定当前买价需要相对第一个收盘价偏离多少点,MoveTwoPoints衡量两个历史收盘价之间的距离。 - 多头条件:当当前买价至少比第一个收盘价低
MoveOnePoints点,且第一个收盘价又至少比第二个收盘价高MoveTwoPoints点时触发。 - 空头条件:当当前买价至少比第一个收盘价高
MoveOnePoints点,且第一个收盘价至少比第二个收盘价低MoveTwoPoints点时触发。 - 策略仅在 K 线收盘后评估信号;如果存在未完成的订单或可用保证金不足,则忽略信号。
头寸管理
- 初始保护:策略不直接挂出止盈止损单,而是追踪入场价格,当价格朝有利方向运行
ProfitPoints(额外加上实时点差)或不利方向运行LossPoints时立即平仓。 - 移动止损:
TrailingPoints大于零时,策略会记录入场后的最佳价位,一旦价格回撤超过该距离便退出头寸。 - 单一持仓:任意时刻只允许持有一个方向的头寸;在平仓订单成交前不会响应新的开仓信号。
仓位管理
- 固定手数:当
UseAutoLot关闭时,每笔交易使用FixedVolume手,并根据交易品种的最小/步长/最大手数自动调整。 - 自动手数:开启自动手数后,会将自由保证金除以
AutoMarginDivider * 1000并四舍五入到最近的整数手。如果出现至少两次连续亏损,则按照lossStreak / DecreaseFactor的比例减少下单手数,从而模拟原始 EA 的递减逻辑。 - 计算出的手数会被限制在
FixedVolume与 99 手之间,并对齐到品种支持的手数步长范围。
其他说明
- 策略实时读取最佳买卖价计算点差,并在多空止盈目标中加上该点差,保持与原脚本一致的收益判定。
- 自由保证金依据账户的
CurrentValue - BlockedValue估算,当缺少保证金信息时退回到当前权益。 - 未额外添加日志或图表,策略可直接在 StockSharp 设计器中优化或在 API 项目里运行。
using System;
using System.Linq;
using System.Collections.Generic;
using Ecng.Common;
using Ecng.Collections;
using Ecng.Serialization;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Reimplementation of the Semilong strategy that compares the current price with two historical closes.
/// Opens a position when the price sharply deviates from older levels and manages the trade with configurable stops,
/// take profit, trailing logic, and loss streak based position sizing.
/// </summary>
public class SemilongWwwForexInstrumentsInfoStrategy : Strategy
{
private readonly StrategyParam<int> _profitPoints;
private readonly StrategyParam<int> _lossPoints;
private readonly StrategyParam<int> _shiftOne;
private readonly StrategyParam<int> _moveOnePoints;
private readonly StrategyParam<int> _shiftTwo;
private readonly StrategyParam<int> _moveTwoPoints;
private readonly StrategyParam<int> _decreaseFactor;
private readonly StrategyParam<decimal> _fixedVolume;
private readonly StrategyParam<int> _trailingPoints;
private readonly StrategyParam<bool> _useAutoLot;
private readonly StrategyParam<int> _autoMarginDivider;
private readonly StrategyParam<DataType> _candleType;
private readonly List<decimal> _closes = new();
private decimal _pipSize;
/// <summary>
/// Initializes a new instance of the <see cref="SemilongWwwForexInstrumentsInfoStrategy"/> class.
/// </summary>
public SemilongWwwForexInstrumentsInfoStrategy()
{
_profitPoints = Param(nameof(ProfitPoints), 120)
.SetDisplay("Take Profit (points)", "Distance in points for the take profit target", "Risk");
_lossPoints = Param(nameof(LossPoints), 60)
.SetDisplay("Stop Loss (points)", "Distance in points for the protective stop", "Risk");
_shiftOne = Param(nameof(ShiftOne), 5)
.SetNotNegative()
.SetDisplay("Primary Shift", "Number of bars between the current close and the comparison close", "Signals");
_moveOnePoints = Param(nameof(MoveOnePoints), 0)
.SetNotNegative()
.SetDisplay("Primary Move (points)", "Minimum deviation in points from the primary shifted close", "Signals");
_shiftTwo = Param(nameof(ShiftTwo), 10)
.SetNotNegative()
.SetDisplay("Secondary Shift", "Additional bars added on top of the primary shift", "Signals");
_moveTwoPoints = Param(nameof(MoveTwoPoints), 0)
.SetNotNegative()
.SetDisplay("Secondary Move (points)", "Minimum distance between the two shifted closes", "Signals");
_decreaseFactor = Param(nameof(DecreaseFactor), 14)
.SetNotNegative()
.SetDisplay("Decrease Factor", "Divisor applied when shrinking the auto lot after losses", "Money Management");
_fixedVolume = Param(nameof(FixedVolume), 1m)
.SetDisplay("Fixed Volume", "Base volume used when auto lot is disabled", "Money Management");
_trailingPoints = Param(nameof(TrailingPoints), 0)
.SetNotNegative()
.SetDisplay("Trailing Stop (points)", "Trailing stop distance in points", "Risk");
_useAutoLot = Param(nameof(UseAutoLot), false)
.SetDisplay("Use Auto Lot", "Enable dynamic position sizing based on free margin", "Money Management");
_autoMarginDivider = Param(nameof(AutoMarginDivider), 7)
.SetRange(1, int.MaxValue)
.SetDisplay("Auto Margin Divider", "Divisor used to convert free margin into the lot size", "Money Management");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Time frame used for signal calculations", "General");
}
/// <summary>
/// Take profit distance expressed in points.
/// </summary>
public int ProfitPoints
{
get => _profitPoints.Value;
set => _profitPoints.Value = value;
}
/// <summary>
/// Stop loss distance expressed in points.
/// </summary>
public int LossPoints
{
get => _lossPoints.Value;
set => _lossPoints.Value = value;
}
/// <summary>
/// Number of bars between the current candle and the primary comparison candle.
/// </summary>
public int ShiftOne
{
get => _shiftOne.Value;
set => _shiftOne.Value = value;
}
/// <summary>
/// Minimum deviation from the primary shifted close required before a trade is allowed.
/// </summary>
public int MoveOnePoints
{
get => _moveOnePoints.Value;
set => _moveOnePoints.Value = value;
}
/// <summary>
/// Additional bars added on top of the the primary shift for the secondary comparison.
/// </summary>
public int ShiftTwo
{
get => _shiftTwo.Value;
set => _shiftTwo.Value = value;
}
/// <summary>
/// Minimum distance in points between the two shifted closes.
/// </summary>
public int MoveTwoPoints
{
get => _moveTwoPoints.Value;
set => _moveTwoPoints.Value = value;
}
/// <summary>
/// Divisor used to reduce the calculated auto lot size after consecutive losses.
/// </summary>
public int DecreaseFactor
{
get => _decreaseFactor.Value;
set => _decreaseFactor.Value = value;
}
/// <summary>
/// Fixed trade volume used whenever auto lot sizing is disabled.
/// </summary>
public decimal FixedVolume
{
get => _fixedVolume.Value;
set => _fixedVolume.Value = value;
}
/// <summary>
/// Trailing stop distance expressed in points.
/// </summary>
public int TrailingPoints
{
get => _trailingPoints.Value;
set => _trailingPoints.Value = value;
}
/// <summary>
/// Gets or sets a value indicating whether the strategy calculates the lot size from free margin.
/// </summary>
public bool UseAutoLot
{
get => _useAutoLot.Value;
set => _useAutoLot.Value = value;
}
/// <summary>
/// Divider applied to free margin when auto lot sizing is enabled.
/// </summary>
public int AutoMarginDivider
{
get => _autoMarginDivider.Value;
set => _autoMarginDivider.Value = value;
}
/// <summary>
/// Candle type processed by the strategy.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_closes.Clear();
_pipSize = 0m;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_pipSize = Security?.PriceStep ?? 0m;
if (_pipSize <= 0m)
_pipSize = 1m;
var subscription = SubscribeCandles(CandleType);
subscription.Bind(ProcessCandleSimple).Start();
StartProtection(
takeProfit: new Unit(2, UnitTypes.Percent),
stopLoss: new Unit(1, UnitTypes.Percent));
}
private void ProcessCandleSimple(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
_closes.Add(candle.ClosePrice);
var totalShift = ShiftOne + ShiftTwo;
if (_closes.Count > totalShift + 2)
_closes.RemoveAt(0);
if (_closes.Count <= totalShift)
return;
if (Position != 0m)
return;
var price = candle.ClosePrice;
var shiftedOneValue = _closes[_closes.Count - 1 - ShiftOne];
var shiftedTwoValue = _closes[_closes.Count - 1 - totalShift];
var moveOne = MoveOnePoints * _pipSize;
var moveTwo = MoveTwoPoints * _pipSize;
var priceDelta = price - shiftedOneValue;
var closeDelta = shiftedOneValue - shiftedTwoValue;
var buySignal = priceDelta < -moveOne && closeDelta > moveTwo;
var sellSignal = priceDelta > moveOne && closeDelta < -moveTwo;
if (buySignal)
{
BuyMarket();
}
else if (sellSignal)
{
SellMarket();
}
}
}
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.Strategies import Strategy
class semilong_www_forex_instruments_info_strategy(Strategy):
"""Compares current price with two historical closes. Opens a position when
the price sharply deviates from older levels. Uses StartProtection for SL/TP."""
def __init__(self):
super(semilong_www_forex_instruments_info_strategy, self).__init__()
self._profit_points = self.Param("ProfitPoints", 120) \
.SetDisplay("Take Profit (points)", "Distance in points for the take profit target", "Risk")
self._loss_points = self.Param("LossPoints", 60) \
.SetDisplay("Stop Loss (points)", "Distance in points for the protective stop", "Risk")
self._shift_one = self.Param("ShiftOne", 5) \
.SetNotNegative() \
.SetDisplay("Primary Shift", "Bars between current close and comparison close", "Signals")
self._move_one_points = self.Param("MoveOnePoints", 0) \
.SetNotNegative() \
.SetDisplay("Primary Move (points)", "Minimum deviation from primary shifted close", "Signals")
self._shift_two = self.Param("ShiftTwo", 10) \
.SetNotNegative() \
.SetDisplay("Secondary Shift", "Additional bars on top of primary shift", "Signals")
self._move_two_points = self.Param("MoveTwoPoints", 0) \
.SetNotNegative() \
.SetDisplay("Secondary Move (points)", "Minimum distance between two shifted closes", "Signals")
self._fixed_volume = self.Param("FixedVolume", 1.0) \
.SetDisplay("Fixed Volume", "Base volume when auto lot is disabled", "Money Management")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))) \
.SetDisplay("Candle Type", "Time frame used for signal calculations", "General")
self._closes = []
self._pip_size = 0.0
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
@property
def ProfitPoints(self):
return self._profit_points.Value
@property
def LossPoints(self):
return self._loss_points.Value
@property
def ShiftOne(self):
return self._shift_one.Value
@property
def MoveOnePoints(self):
return self._move_one_points.Value
@property
def ShiftTwo(self):
return self._shift_two.Value
@property
def MoveTwoPoints(self):
return self._move_two_points.Value
@property
def FixedVolume(self):
return self._fixed_volume.Value
def OnReseted(self):
super(semilong_www_forex_instruments_info_strategy, self).OnReseted()
self._closes = []
self._pip_size = 0.0
def OnStarted2(self, time):
super(semilong_www_forex_instruments_info_strategy, self).OnStarted2(time)
step = self.Security.PriceStep if self.Security is not None else 0.0
if step is None or float(step) <= 0:
self._pip_size = 1.0
else:
self._pip_size = float(step)
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self._process_candle).Start()
self.StartProtection(
Unit(2, UnitTypes.Percent),
Unit(1, UnitTypes.Percent))
def _process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
close = float(candle.ClosePrice)
self._closes.append(close)
total_shift = self.ShiftOne + self.ShiftTwo
if len(self._closes) > total_shift + 2:
self._closes.pop(0)
if len(self._closes) <= total_shift:
return
if self.Position != 0:
return
shifted_one_value = self._closes[len(self._closes) - 1 - self.ShiftOne]
shifted_two_value = self._closes[len(self._closes) - 1 - total_shift]
move_one = float(self.MoveOnePoints) * self._pip_size
move_two = float(self.MoveTwoPoints) * self._pip_size
price_delta = close - shifted_one_value
close_delta = shifted_one_value - shifted_two_value
buy_signal = price_delta < -move_one and close_delta > move_two
sell_signal = price_delta > move_one and close_delta < -move_two
if buy_signal:
self.BuyMarket()
elif sell_signal:
self.SellMarket()
def CreateClone(self):
return semilong_www_forex_instruments_info_strategy()