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