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;
        }
    }
}