Bot for Spot Market - Custom Grid 策略
Bot for Spot Market - Custom Grid 策略先建立初始头寸,当价格跌破上次买入价的指定百分比时加仓。当价格高于平均买入价达到设定的盈利百分比且持仓盈利时,策略会全部平仓。
详情
- 入场条件:
- 在开始时间买入。
- 当价格比最近一次买入价低
NextEntryPercent% 时加仓。
- 方向: 仅做多。
- 出场条件:
- 当价格高于平均买入价
ProfitPercent% 且持仓盈利时全部平仓。
- 当价格高于平均买入价
- 止损: 无。
- 默认值:
OrderValue= 10MinAmountMovement= 0.00001Rounding= 5NextEntryPercent= 0.5ProfitPercent= 2
- 过滤器:
- 分类: 网格交易
- 方向: 多头
- 指标: 无
- 止损: 无
- 复杂度: 低
- 时间框架: 任意
- 季节性: 否
- 神经网络: 否
- 背离: 否
- 风险等级: 中等
using System;
using System.Collections.Generic;
using Ecng.Common;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Bot for Spot Market - Custom Grid strategy.
/// Long only: buys an OrderValue worth of the asset as soon as it is flat, adds another OrderValue whenever the close drops
/// NextEntryPercent below the last entry price, and sells the whole position once the close exceeds the average entry price
/// by ProfitPercent.
/// </summary>
public class BotForSpotMarketCustomGridStrategy : Strategy
{
private readonly StrategyParam<decimal> _orderValue;
private readonly StrategyParam<decimal> _minAmountMovement;
private readonly StrategyParam<int> _rounding;
private readonly StrategyParam<decimal> _nextEntryPercent;
private readonly StrategyParam<decimal> _profitPercent;
private readonly StrategyParam<DataType> _candleType;
private decimal _lastEntryPrice;
private decimal _avgPrice;
private decimal _boughtVolume;
/// <summary>
/// Value of each order in quote currency.
/// </summary>
public decimal OrderValue
{
get => _orderValue.Value;
set => _orderValue.Value = value;
}
/// <summary>
/// Smallest amount increment added to the rounded quantity.
/// </summary>
public decimal MinAmountMovement
{
get => _minAmountMovement.Value;
set => _minAmountMovement.Value = value;
}
/// <summary>
/// Decimal places the quantity is rounded to.
/// </summary>
public int Rounding
{
get => _rounding.Value;
set => _rounding.Value = value;
}
/// <summary>
/// Drop below the last entry price, in percent, that adds a new order.
/// </summary>
public decimal NextEntryPercent
{
get => _nextEntryPercent.Value;
set => _nextEntryPercent.Value = value;
}
/// <summary>
/// Rise above the average entry price, in percent, that closes the position.
/// </summary>
public decimal ProfitPercent
{
get => _profitPercent.Value;
set => _profitPercent.Value = value;
}
/// <summary>
/// Candle type.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Constructor.
/// </summary>
public BotForSpotMarketCustomGridStrategy()
{
_orderValue = Param(nameof(OrderValue), 10m)
.SetGreaterThanZero()
.SetDisplay("Order Value", "Value of each order in quote currency", "Parameters");
_minAmountMovement = Param(nameof(MinAmountMovement), 0.00001m)
.SetNotNegative()
.SetDisplay("Min Amount Movement", "Smallest amount increment added to the rounded quantity", "Parameters");
_rounding = Param(nameof(Rounding), 5)
.SetNotNegative()
.SetDisplay("Rounding", "Decimal places the quantity is rounded to", "Parameters");
_nextEntryPercent = Param(nameof(NextEntryPercent), 0.5m)
.SetGreaterThanZero()
.SetDisplay("Next Entry Less Than (%)", "Drop below the last entry price that adds a new order", "Parameters");
_profitPercent = Param(nameof(ProfitPercent), 2m)
.SetGreaterThanZero()
.SetDisplay("Profit (%)", "Rise above the average entry price that closes the position", "Parameters");
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
.SetDisplay("Candle Type", "Type of candles to use", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
ResetGrid();
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
ResetGrid();
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawOwnTrades(area);
}
}
private void ResetGrid()
{
_lastEntryPrice = 0;
_avgPrice = 0;
_boughtVolume = 0;
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
if (!IsFormedAndOnlineAndAllowTrading())
return;
var price = candle.ClosePrice;
if (Position <= 0)
{
BuyMarket(GetQuantity(price));
return;
}
if (_lastEntryPrice > 0 && price < _lastEntryPrice * (1m - NextEntryPercent / 100m))
{
BuyMarket(GetQuantity(price));
return;
}
if (_avgPrice > 0 && price > _avgPrice * (1m + ProfitPercent / 100m))
SellMarket(Position);
}
private decimal GetQuantity(decimal price)
{
var raw = OrderValue / price;
var rounded = Math.Round(raw, Rounding);
var quantity = rounded >= raw ? rounded + MinAmountMovement : rounded + MinAmountMovement * 2m;
// The instrument cannot accept less than its minimum volume or a fraction of its volume step.
if (Security?.VolumeStep is decimal step && step > 0)
quantity = Math.Ceiling(quantity / step) * step;
if (Security?.MinVolume is decimal minVolume && quantity < minVolume)
quantity = minVolume;
return quantity;
}
/// <inheritdoc />
protected override void OnOwnTradeReceived(MyTrade trade)
{
base.OnOwnTradeReceived(trade);
if (trade.Order == null || trade.Trade == null)
return;
var price = trade.Trade.Price;
var volume = trade.Trade.Volume;
if (trade.Order.Side == Sides.Buy)
{
var newVolume = _boughtVolume + volume;
if (newVolume > 0)
_avgPrice = (_avgPrice * _boughtVolume + price * volume) / newVolume;
_boughtVolume = newVolume;
_lastEntryPrice = price;
}
else if (Position <= 0)
{
ResetGrid();
}
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Strategies")
clr.AddReference("StockSharp.BusinessEntities")
from System import TimeSpan, Decimal, Math
from StockSharp.Messages import DataType, CandleStates, Sides
from StockSharp.Algo.Strategies import Strategy
class bot_for_spot_market_custom_grid_strategy(Strategy):
"""
Bot for Spot Market - Custom Grid strategy.
Long only: buys an OrderValue worth of the asset as soon as it is flat, adds another OrderValue whenever the close drops
NextEntryPercent below the last entry price, and sells the whole position once the close exceeds the average entry price
by ProfitPercent.
"""
def __init__(self):
super(bot_for_spot_market_custom_grid_strategy, self).__init__()
self._order_value = self.Param("OrderValue", 10.0).SetGreaterThanZero().SetDisplay("Order Value", "Value of each order in quote currency", "Parameters")
self._min_amount_movement = self.Param("MinAmountMovement", 0.00001).SetNotNegative().SetDisplay("Min Amount Movement", "Smallest amount increment added to the rounded quantity", "Parameters")
self._rounding = self.Param("Rounding", 5).SetNotNegative().SetDisplay("Rounding", "Decimal places the quantity is rounded to", "Parameters")
self._next_entry_percent = self.Param("NextEntryPercent", 0.5).SetGreaterThanZero().SetDisplay("Next Entry Less Than (%)", "Drop below the last entry price that adds a new order", "Parameters")
self._profit_percent = self.Param("ProfitPercent", 2.0).SetGreaterThanZero().SetDisplay("Profit (%)", "Rise above the average entry price that closes the position", "Parameters")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(1))).SetDisplay("Candle Type", "Type of candles to use", "General")
self._reset_grid()
@property
def candle_type(self):
return self._candle_type.Value
def _reset_grid(self):
self._last_entry_price = Decimal(0)
self._avg_price = Decimal(0)
self._bought_volume = Decimal(0)
def OnReseted(self):
super(bot_for_spot_market_custom_grid_strategy, self).OnReseted()
self._reset_grid()
def OnStarted2(self, time):
super(bot_for_spot_market_custom_grid_strategy, self).OnStarted2(time)
self._reset_grid()
subscription = self.SubscribeCandles(self.candle_type)
subscription.Bind(self._process_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawOwnTrades(area)
def _process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
if not self.IsFormedAndOnlineAndAllowTrading():
return
price = candle.ClosePrice
hundred = Decimal(100)
if self.Position <= 0:
self.BuyMarket(self._get_quantity(price))
return
next_entry = Decimal(self._next_entry_percent.Value)
if self._last_entry_price > 0 and price < self._last_entry_price * (Decimal(1) - next_entry / hundred):
self.BuyMarket(self._get_quantity(price))
return
profit = Decimal(self._profit_percent.Value)
if self._avg_price > 0 and price > self._avg_price * (Decimal(1) + profit / hundred):
self.SellMarket(self.Position)
def _get_quantity(self, price):
min_move = Decimal(self._min_amount_movement.Value)
raw = Decimal(self._order_value.Value) / price
rounded = Math.Round(raw, int(self._rounding.Value))
quantity = rounded + min_move if rounded >= raw else rounded + min_move * Decimal(2)
# The instrument cannot accept less than its minimum volume or a fraction of its volume step.
security = self.Security
if security is not None:
step = security.VolumeStep
if step is not None and step > 0:
quantity = Math.Ceiling(quantity / step) * step
min_volume = security.MinVolume
if min_volume is not None and quantity < min_volume:
quantity = min_volume
return quantity
def OnOwnTradeReceived(self, trade):
super(bot_for_spot_market_custom_grid_strategy, self).OnOwnTradeReceived(trade)
if trade.Order is None or trade.Trade is None:
return
price = trade.Trade.TradePrice
volume = trade.Trade.TradeVolume
if price is None or volume is None:
return
if trade.Order.Side == Sides.Buy:
new_volume = self._bought_volume + volume
if new_volume > 0:
self._avg_price = (self._avg_price * self._bought_volume + price * volume) / new_volume
self._bought_volume = new_volume
self._last_entry_price = price
elif self.Position <= 0:
self._reset_grid()
def CreateClone(self):
return bot_for_spot_market_custom_grid_strategy()