Estrategia del Modelo Captain Backtest
Rastrea el rango de precios de la sesión temprana para establecer un sesgo diario. Opera rupturas durante la ventana de operaciones después de un retroceso.
Detalles
- Sesgo: El máximo o mínimo del rango matutino define el sesgo largo o corto.
- Entrada: Ruptura por encima/debajo de la vela anterior una vez cumplidas las condiciones de retroceso.
- Largo/Corto: Ambos.
- Salida: Riesgo/recompensa fijo o fin de la ventana de operaciones.
- Stops: Distancia fija en puntos.
- Valores predeterminados:
- PrevRangeStart = 06:00
- PrevRangeEnd = 10:00
- TakeStart = 10:00
- TakeEnd = 11:15
- TradeStart = 10:00
- TradeEnd = 16:00
- Risk = 25
- Reward = 75
using System;
using System.Collections.Generic;
using Ecng.Common;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Captain Backtest Model.
/// Builds the morning range, fixes the day's bias on the first range break,
/// waits for a retracement, then enters on a close through the previous candle.
/// </summary>
public class CaptainBacktestModelStrategy : Strategy
{
private readonly StrategyParam<TimeSpan> _prevRangeStart;
private readonly StrategyParam<TimeSpan> _prevRangeEnd;
private readonly StrategyParam<TimeSpan> _takeStart;
private readonly StrategyParam<TimeSpan> _takeEnd;
private readonly StrategyParam<TimeSpan> _tradeStart;
private readonly StrategyParam<TimeSpan> _tradeEnd;
private readonly StrategyParam<decimal> _risk;
private readonly StrategyParam<decimal> _reward;
private readonly StrategyParam<DataType> _candleType;
private DateTime _sessionDate;
private decimal? _rangeHigh;
private decimal? _rangeLow;
private int _bias;
private bool _retracementSeen;
private bool _tradedToday;
private ICandleMessage _previousCandle;
private decimal _entryPrice;
public TimeSpan PrevRangeStart { get => _prevRangeStart.Value; set => _prevRangeStart.Value = value; }
public TimeSpan PrevRangeEnd { get => _prevRangeEnd.Value; set => _prevRangeEnd.Value = value; }
public TimeSpan TakeStart { get => _takeStart.Value; set => _takeStart.Value = value; }
public TimeSpan TakeEnd { get => _takeEnd.Value; set => _takeEnd.Value = value; }
public TimeSpan TradeStart { get => _tradeStart.Value; set => _tradeStart.Value = value; }
public TimeSpan TradeEnd { get => _tradeEnd.Value; set => _tradeEnd.Value = value; }
public decimal Risk { get => _risk.Value; set => _risk.Value = value; }
public decimal Reward { get => _reward.Value; set => _reward.Value = value; }
public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
public CaptainBacktestModelStrategy()
{
_prevRangeStart = Param(nameof(PrevRangeStart), new TimeSpan(6, 0, 0))
.SetDisplay("Previous Range Start", "Start of the range used to establish the daily bias.", "Timing");
_prevRangeEnd = Param(nameof(PrevRangeEnd), new TimeSpan(10, 0, 0))
.SetDisplay("Previous Range End", "End of the range used to establish the daily bias.", "Timing");
_takeStart = Param(nameof(TakeStart), new TimeSpan(10, 0, 0))
.SetDisplay("Take Start", "Earliest time when a range break may establish the daily bias.", "Timing");
_takeEnd = Param(nameof(TakeEnd), new TimeSpan(11, 15, 0))
.SetDisplay("Take End", "Latest time when a range break may establish the daily bias.", "Timing");
_tradeStart = Param(nameof(TradeStart), new TimeSpan(10, 0, 0))
.SetDisplay("Trade Start", "Start of the entry window.", "Timing");
_tradeEnd = Param(nameof(TradeEnd), new TimeSpan(16, 0, 0))
.SetDisplay("Trade End", "End of the entry window and forced-exit time.", "Timing");
_risk = Param(nameof(Risk), 25m)
.SetGreaterThanZero()
.SetDisplay("Risk", "Fixed stop distance in price points.", "Risk");
_reward = Param(nameof(Reward), 75m)
.SetGreaterThanZero()
.SetDisplay("Reward", "Fixed profit-target distance in price points.", "Risk");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Candle series used by the model.", "General");
}
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
=> [(Security, CandleType)];
protected override void OnReseted()
{
base.OnReseted();
ResetSession(default);
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
ResetSession(default);
var subscription = SubscribeCandles(CandleType);
subscription.Bind(ProcessCandle).Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
var date = candle.OpenTime.Date;
if (_sessionDate != date)
{
if (_sessionDate != default && Position != 0)
ClosePosition();
ResetSession(date);
}
var time = candle.OpenTime.TimeOfDay;
// Step 1: collect the 06:00-10:00 range (end is exclusive).
if (time >= PrevRangeStart && time < PrevRangeEnd)
{
_rangeHigh = _rangeHigh is decimal high ? Math.Max(high, candle.HighPrice) : candle.HighPrice;
_rangeLow = _rangeLow is decimal low ? Math.Min(low, candle.LowPrice) : candle.LowPrice;
_previousCandle = candle;
return;
}
// Fixed R:R exits. If both levels are touched inside one candle, use the adverse
// level first because the intrabar path is unknown.
if (Position > 0 && _entryPrice > 0m)
{
if (candle.LowPrice <= _entryPrice - Risk || candle.HighPrice >= _entryPrice + Reward)
{
ClosePosition();
_previousCandle = candle;
return;
}
}
else if (Position < 0 && _entryPrice > 0m)
{
if (candle.HighPrice >= _entryPrice + Risk || candle.LowPrice <= _entryPrice - Reward)
{
ClosePosition();
_previousCandle = candle;
return;
}
}
// The trade window also acts as the time exit.
if (time >= TradeEnd)
{
if (Position != 0)
ClosePosition();
_previousCandle = candle;
return;
}
// Step 2: the first side of the morning range taken between TakeStart and TakeEnd
// fixes the bias. A candle taking both sides is ambiguous, so no bias is assigned.
if (_bias == 0 && _rangeHigh is decimal rangeHigh && _rangeLow is decimal rangeLow &&
time >= TakeStart && time <= TakeEnd)
{
var brokeHigh = candle.HighPrice > rangeHigh;
var brokeLow = candle.LowPrice < rangeLow;
if (brokeHigh ^ brokeLow)
_bias = brokeHigh ? 1 : -1;
}
// No bias by TakeEnd means no trade for this session.
if (_bias == 0 || _tradedToday || time < TradeStart || time > TradeEnd || _previousCandle is null)
{
_previousCandle = candle;
return;
}
// Step 3: after the bias is known, wait for a pullback. The model's two
// documented retracement definitions are an opposite-colour close or a sweep
// of the previous candle's opposite extreme.
if (_bias > 0)
{
if (candle.ClosePrice < candle.OpenPrice || candle.LowPrice < _previousCandle.LowPrice)
_retracementSeen = true;
if (_retracementSeen && candle.ClosePrice > _previousCandle.HighPrice)
Enter(Sides.Buy, candle.ClosePrice);
}
else
{
if (candle.ClosePrice > candle.OpenPrice || candle.HighPrice > _previousCandle.HighPrice)
_retracementSeen = true;
if (_retracementSeen && candle.ClosePrice < _previousCandle.LowPrice)
Enter(Sides.Sell, candle.ClosePrice);
}
_previousCandle = candle;
}
private void Enter(Sides side, decimal price)
{
if (_tradedToday)
return;
_tradedToday = true;
_entryPrice = price;
if (side == Sides.Buy)
BuyMarket();
else
SellMarket();
}
private void ResetSession(DateTime date)
{
_sessionDate = date;
_rangeHigh = null;
_rangeLow = null;
_bias = 0;
_retracementSeen = false;
_tradedToday = false;
_previousCandle = null;
_entryPrice = 0m;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan
from StockSharp.Algo.Strategies import Strategy
from StockSharp.Messages import DataType, CandleStates, Sides
class captain_backtest_model_strategy(Strategy):
"""
Captain Backtest Model.
Builds the morning range, fixes the day's bias on the first range break,
waits for a retracement, then enters on a close through the previous candle.
"""
def __init__(self):
super(captain_backtest_model_strategy, self).__init__()
self._prev_range_start = self.Param("PrevRangeStart", TimeSpan(6, 0, 0))
self._prev_range_end = self.Param("PrevRangeEnd", TimeSpan(10, 0, 0))
self._take_start = self.Param("TakeStart", TimeSpan(10, 0, 0))
self._take_end = self.Param("TakeEnd", TimeSpan(11, 15, 0))
self._trade_start = self.Param("TradeStart", TimeSpan(10, 0, 0))
self._trade_end = self.Param("TradeEnd", TimeSpan(16, 0, 0))
self._risk = self.Param("Risk", 25.0)
self._reward = self.Param("Reward", 75.0)
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5)))
self._session_date = None
self._range_high = None
self._range_low = None
self._bias = 0
self._retracement_seen = False
self._traded_today = False
self._previous_candle = None
self._entry_price = 0.0
@property
def CandleType(self):
return self._candle_type.Value
def GetWorkingSecurities(self):
return [(self.Security, self.CandleType)]
def OnReseted(self):
super(captain_backtest_model_strategy, self).OnReseted()
self._reset_session(None)
def OnStarted2(self, time):
super(captain_backtest_model_strategy, self).OnStarted2(time)
self._reset_session(None)
subscription = self.SubscribeCandles(self.CandleType)
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
date = candle.OpenTime.Date
if self._session_date != date:
if self._session_date is not None and self.Position != 0:
self.ClosePosition()
self._reset_session(date)
t = candle.OpenTime.TimeOfDay
prev_range_start = self._prev_range_start.Value
prev_range_end = self._prev_range_end.Value
take_start = self._take_start.Value
take_end = self._take_end.Value
trade_start = self._trade_start.Value
trade_end = self._trade_end.Value
risk = float(self._risk.Value)
reward = float(self._reward.Value)
if t >= prev_range_start and t < prev_range_end:
high = float(candle.HighPrice)
low = float(candle.LowPrice)
self._range_high = high if self._range_high is None else max(self._range_high, high)
self._range_low = low if self._range_low is None else min(self._range_low, low)
self._previous_candle = candle
return
if self.Position > 0 and self._entry_price > 0:
if float(candle.LowPrice) <= self._entry_price - risk or float(candle.HighPrice) >= self._entry_price + reward:
self.ClosePosition()
self._previous_candle = candle
return
elif self.Position < 0 and self._entry_price > 0:
if float(candle.HighPrice) >= self._entry_price + risk or float(candle.LowPrice) <= self._entry_price - reward:
self.ClosePosition()
self._previous_candle = candle
return
if t >= trade_end:
if self.Position != 0:
self.ClosePosition()
self._previous_candle = candle
return
if self._bias == 0 and self._range_high is not None and self._range_low is not None and t >= take_start and t <= take_end:
broke_high = float(candle.HighPrice) > self._range_high
broke_low = float(candle.LowPrice) < self._range_low
if broke_high != broke_low:
self._bias = 1 if broke_high else -1
if self._bias == 0 or self._traded_today or t < trade_start or t > trade_end or self._previous_candle is None:
self._previous_candle = candle
return
if self._bias > 0:
if float(candle.ClosePrice) < float(candle.OpenPrice) or float(candle.LowPrice) < float(self._previous_candle.LowPrice):
self._retracement_seen = True
if self._retracement_seen and float(candle.ClosePrice) > float(self._previous_candle.HighPrice):
self._enter(Sides.Buy, float(candle.ClosePrice))
else:
if float(candle.ClosePrice) > float(candle.OpenPrice) or float(candle.HighPrice) > float(self._previous_candle.HighPrice):
self._retracement_seen = True
if self._retracement_seen and float(candle.ClosePrice) < float(self._previous_candle.LowPrice):
self._enter(Sides.Sell, float(candle.ClosePrice))
self._previous_candle = candle
def _enter(self, side, price):
if self._traded_today:
return
self._traded_today = True
self._entry_price = price
if side == Sides.Buy:
self.BuyMarket()
else:
self.SellMarket()
def _reset_session(self, date):
self._session_date = date
self._range_high = None
self._range_low = None
self._bias = 0
self._retracement_seen = False
self._traded_today = False
self._previous_candle = None
self._entry_price = 0.0
def CreateClone(self):
return captain_backtest_model_strategy()