| Overall Statistics |
|
Total Trades 450 Average Win 0.24% Average Loss -0.18% Compounding Annual Return 6.211% Drawdown 2.800% Expectancy 0.661 Net Profit 29.839% Sharpe Ratio 1.252 Probabilistic Sharpe Ratio 76.130% Loss Rate 29% Win Rate 71% Profit-Loss Ratio 1.34 Alpha 0 Beta 0 Annual Standard Deviation 0.034 Annual Variance 0.001 Information Ratio 1.252 Tracking Error 0.034 Treynor Ratio 0 Total Fees $513.57 Estimated Strategy Capacity $140000000.00 Lowest Capacity Asset SPY R735QTJ8XC9X Portfolio Turnover 16.71% |
using KeltnerChannelOutOfRange;
using QuantConnect.Algorithm.Framework.Portfolio;
using QuantConnect.Data;
using QuantConnect.Indicators;
using QuantConnect.Orders;
using System;
using System.Collections.Concurrent;
using System.ComponentModel.DataAnnotations;
using System.Linq;
namespace QuantConnect.Algorithm.CSharp
{
public class KeltnerChannelOutOfRange : QCAlgorithm
{
const decimal PercentageOfAssetsPerOrder = 0.9m;
private Symbol _symbol;
private KeltnerChannels _keltnerChannel;
private RollingWindow<IndicatorDataPoint> _kelnerRollingWindow;
public override void Initialize()
{
SetStartDate(2019, 1, 1); // Set Start Date
SetEndDate(2023, 5, 1); // Set Start Date
SetCash(100000); // Set Strategy Cash
_symbol = AddEquity("SPY", Resolution.Hour, dataNormalizationMode: DataNormalizationMode.Raw).Symbol;
_kelnerRollingWindow = new RollingWindow<IndicatorDataPoint>(3);
_keltnerChannel = KCH(_symbol, 20, 2.25m, MovingAverageType.Exponential);
_keltnerChannel.Updated += (sender, updated) => _kelnerRollingWindow.Add(updated);
SetBrokerageModel(Brokerages.BrokerageName.InteractiveBrokersBrokerage, AccountType.Margin);
SetWarmUp(90);
}
public override void OnData(Slice data)
{
if (!data.Bars.ContainsKey(_symbol))
{
return;
}
if (!_keltnerChannel.IsReady || !_kelnerRollingWindow.IsReady)
{
return;
}
var takeProfitTickets = Transactions.GetOrderTickets(c => c.Tag == "tp" && c.Status != OrderStatus.Filled);
foreach (var ticket in takeProfitTickets)
{
var bandValue = ticket.IsLong()
? _keltnerChannel.UpperBand.Current
: _keltnerChannel.LowerBand.Current;
var price = Math.Round(bandValue, 2);
ticket.UpdateLimitPrice(price);
Debug($"Update limit price for take profit order: {price}");
}
var isLong = Portfolio[_symbol].Quantity >= 0;
var isShort = Portfolio[_symbol].Quantity <= 0;
var bar = data.Bars[_symbol];
// sell existing holdings
if (bar.Low > _keltnerChannel.UpperBand.Current && isLong)
{
var stocksCount = PortfolioTarget.Percent(this, _symbol, PercentageOfAssetsPerOrder / 2).Quantity; // allocate two times less funds for sharing because it's more risky
var price = Math.Round(bar.High, 2);
var t = LimitOrder(_symbol, -stocksCount, price);
Debug($"Set sell limit order at {price}");
}
if (bar.High < _keltnerChannel.LowerBand.Current && isShort)
{
var stocksCount = PortfolioTarget.Percent(this, _symbol, PercentageOfAssetsPerOrder).Quantity;
var price = Math.Round(bar.Low, 2);
LimitOrder(_symbol, stocksCount, price);
Debug($"Set buy limit order at {price}");
}
}
ConcurrentDictionary<int, OrderTicket> orders = new ConcurrentDictionary<int, OrderTicket>();
public override void OnOrderEvent(OrderEvent orderEvent)
{
if (orderEvent.Status == OrderStatus.Filled)
{
if (orders.ContainsKey(orderEvent.OrderId))
{
Debug("Take-profit limit orer was executed");
orders.TryRemove(orderEvent.OrderId, out _);
}
else
{
var orderTicket = Transactions.GetOrderTicket(orderEvent.OrderId);
if (orderTicket != null)
{
var bandValue = orderTicket.IsLong()
? _keltnerChannel.UpperBand.Current
: _keltnerChannel.LowerBand.Current;
var price = Math.Round(bandValue, 2);
var ticket = LimitOrder(_symbol, -orderEvent.Quantity, price, "tp");
orders.TryAdd(ticket.OrderId, ticket);
Debug($"Open limit take-profit order {price}");
}
}
}
}
}
}
using QuantConnect.Orders;
namespace KeltnerChannelOutOfRange
{
public static class OrderTickeExtensions
{
public static bool IsLong(this OrderTicket orderTicket)
{
return orderTicket.Quantity > 0;
}
public static bool IsShort(this OrderTicket orderTicket)
{
return orderTicket.Quantity < 0;
}
}
}