Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature 7135: Add Support for Trailing Stop Limit Order #8399

Open
wants to merge 16 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 105 additions & 23 deletions Algorithm.CSharp/OrderTicketDemoAlgorithm.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ public class OrderTicketDemoAlgorithm : QCAlgorithm, IRegressionAlgorithmDefinit
private readonly List<OrderTicket> _openStopMarketOrders = new List<OrderTicket>();
private readonly List<OrderTicket> _openStopLimitOrders = new List<OrderTicket>();
private readonly List<OrderTicket> _openTrailingStopOrders = new List<OrderTicket>();
private readonly List<OrderTicket> _openTrailingStopLimitOrders = new List<OrderTicket>();

/// <summary>
/// Initialise the data and resolution required, as well as the cash and start-end dates for your algorithm. All algorithms must initialized.
Expand Down Expand Up @@ -86,6 +87,10 @@ public override void OnData(Slice slice)
// MARKET ON CLOSE ORDERS

MarketOnCloseOrders();

// TRAILING STOP LIMIT ORDERS

TrailingStopLimitOrders();
}

/// <summary>
Expand Down Expand Up @@ -418,6 +423,84 @@ private void TrailingStopOrders()
}
}

/// <summary>
/// TrailingStopLimitOrders work the same way as StopLimitOrders, except
/// their stop price is adjusted to a certain amount, keeping it a certain
/// fixed distance from/to the market price, depending on the order direction.
/// The limit price adjusts based on a limit offset compared to the stop price.
/// You can submit requests to update or cancel the StopLimitOrder at any time.
/// The stop price, trailing amount, limit price and limit offset for an order
/// can be retrieved from the ticket using the OrderTicket.Get(OrderField) method, for example:
/// <code>
/// var currentStopPrice = orderTicket.Get(OrderField.StopPrice);
/// var trailingAmount = orderTicket.Get(OrderField.TrailingAmount);
/// var currentLimitPrice = orderTicket.Get(OrderField.LimitPrice);
/// var limitOffset = orderTicket.Get(OrderField.LimitOffset);
/// </code>
/// </summary>
private void TrailingStopLimitOrders()
{
if (TimeIs(7, 12, 0))
{
Log("Submitting TrailingStopLimitOrder");

// a long stop is triggered when the price rises above the value
// so we'll set a long stop .25% above the current bar's close

var close = Securities[symbol].Close;
var stopPrice = close * 1.0025m;
var limitPrice = stopPrice + 0.1m;
var newTicket = TrailingStopLimitOrder(symbol, 10, stopPrice, limitPrice, trailingAmount: 0.0025m, trailingAsPercentage: true, 0.1m);
_openTrailingStopLimitOrders.Add(newTicket);

// a short stop is triggered when the price falls below the value
// so we'll set a short stop .25% below the current bar's close

stopPrice = close * .9975m;
limitPrice = stopPrice - 0.1m;
newTicket = TrailingStopLimitOrder(symbol, -10, stopPrice, limitPrice, trailingAmount: 0.0025m, trailingAsPercentage: true, 0.1m);
_openTrailingStopLimitOrders.Add(newTicket);
}

// when we submitted new trailing stop limit orders we placed them into this list,
// so while there's two entries they're still open and need processing
else if (_openTrailingStopLimitOrders.Count == 2)
{
// check if either is filled and cancel the other
var longOrder = _openTrailingStopLimitOrders[0];
var shortOrder = _openTrailingStopLimitOrders[1];
if (CheckPairOrdersForFills(longOrder, shortOrder))
{
_openTrailingStopLimitOrders.Clear();
return;
}

// if neither order has filled in the last 5 minutes, bring in the trailing percentage by 0.01%
if ((UtcTime - longOrder.Time).TotalMinutes % 5 != 0)
{
return;
}
var longTrailingPercentage = longOrder.Get(OrderField.TrailingAmount);
var newLongTrailingPercentage = Math.Max(longTrailingPercentage - 0.0001m, 0.0001m);
var shortTrailingPercentage = shortOrder.Get(OrderField.TrailingAmount);
var newShortTrailingPercentage = Math.Max(shortTrailingPercentage - 0.0001m, 0.0001m);
Log($"Updating trailing percentages - Long: {newLongTrailingPercentage.ToStringInvariant("0.000")} Short: {newShortTrailingPercentage.ToStringInvariant("0.000")}");

longOrder.Update(new UpdateOrderFields
{
// we could change the quantity, but need to specify it
//Quantity =
TrailingAmount = newLongTrailingPercentage,
Tag = "Update #" + (longOrder.UpdateRequests.Count + 1)
});
shortOrder.Update(new UpdateOrderFields
{
TrailingAmount = newShortTrailingPercentage,
Tag = "Update #" + (shortOrder.UpdateRequests.Count + 1)
});
}
}

/// <summary>
/// MarketOnCloseOrders are always executed at the next market's closing
/// price. The only properties that can be updated are the quantity and
Expand Down Expand Up @@ -503,7 +586,6 @@ private void MarketOnOpenOrders()
Tag = "Update #" + (ticket.UpdateRequests.Count + 1)
});
}

}

public override void OnOrderEvent(OrderEvent orderEvent)
Expand Down Expand Up @@ -572,9 +654,9 @@ public override void OnEndOfAlgorithm()
var openOrderTickets = Transactions.GetOpenOrderTickets(basicOrderTicketFilter);
var remainingOpenOrders = Transactions.GetOpenOrdersRemainingQuantity(basicOrderTicketFilter);

if (filledOrders.Count() != 9 || orderTickets.Count() != 12)
if (filledOrders.Count() != 10 || orderTickets.Count() != 14)
{
throw new RegressionTestException($"There were expected 9 filled orders and 12 order tickets");
throw new RegressionTestException($"There were expected 10 filled orders and 14 order tickets");
}
if (openOrders.Count != 0 || openOrderTickets.Any())
{
Expand Down Expand Up @@ -604,9 +686,9 @@ public override void OnEndOfAlgorithm()
var defaultOpenOrderTickets = Transactions.GetOpenOrderTickets();
var defaultOpenOrdersRemaining = Transactions.GetOpenOrdersRemainingQuantity();

if (defaultOrders.Count() != 12 || defaultOrderTickets.Count() != 12)
if (defaultOrders.Count() != 14 || defaultOrderTickets.Count() != 14)
{
throw new RegressionTestException($"There were expected 12 orders and 12 order tickets");
throw new RegressionTestException($"There were expected 14 orders and 14 order tickets");
}
if (defaultOpenOrders.Count != 0 || defaultOpenOrderTickets.Any())
{
Expand Down Expand Up @@ -648,33 +730,33 @@ public override void OnEndOfAlgorithm()
/// </summary>
public Dictionary<string, string> ExpectedStatistics => new Dictionary<string, string>
{
{"Total Orders", "12"},
{"Total Orders", "14"},
{"Average Win", "0%"},
{"Average Loss", "-0.01%"},
{"Compounding Annual Return", "77.184%"},
{"Average Loss", "0.00%"},
{"Compounding Annual Return", "63.380%"},
{"Drawdown", "0.100%"},
{"Expectancy", "-1"},
{"Start Equity", "100000"},
{"End Equity", "100734.03"},
{"Net Profit", "0.734%"},
{"Sharpe Ratio", "12.597"},
{"Sortino Ratio", "464.862"},
{"Probabilistic Sharpe Ratio", "99.521%"},
{"End Equity", "100629.62"},
{"Net Profit", "0.630%"},
{"Sharpe Ratio", "12.445"},
{"Sortino Ratio", "680.042"},
{"Probabilistic Sharpe Ratio", "99.827%"},
{"Loss Rate", "100%"},
{"Win Rate", "0%"},
{"Profit-Loss Ratio", "0"},
{"Alpha", "0.2"},
{"Beta", "0.195"},
{"Annual Standard Deviation", "0.047"},
{"Alpha", "0.165"},
{"Beta", "0.161"},
{"Annual Standard Deviation", "0.039"},
{"Annual Variance", "0.002"},
{"Information Ratio", "-7.724"},
{"Tracking Error", "0.18"},
{"Treynor Ratio", "3.002"},
{"Total Fees", "$9.00"},
{"Estimated Strategy Capacity", "$49000000.00"},
{"Information Ratio", "-7.97"},
{"Tracking Error", "0.187"},
{"Treynor Ratio", "2.998"},
{"Total Fees", "$10.00"},
{"Estimated Strategy Capacity", "$51000000.00"},
{"Lowest Capacity Asset", "SPY R735QTJ8XC9X"},
{"Portfolio Turnover", "7.18%"},
{"OrderListHash", "d1ed6571d5895f4c951d287b2903f561"}
{"Portfolio Turnover", "6.90%"},
{"OrderListHash", "4a84e8f5608a8a32ff95d0004a35a822"}
};
}
}
56 changes: 54 additions & 2 deletions Algorithm.CSharp/SplitEquityRegressionAlgorithm.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ public override void OnData(Slice slice)
_tickets.Add(StopLimitOrder(_aapl, 10, 15, 15));
_tickets.Add(TrailingStopOrder(_aapl, 10, 1000, 60m, trailingAsPercentage: false));
_tickets.Add(TrailingStopOrder(_aapl, 10, 1000, 0.1m, trailingAsPercentage: true));
_tickets.Add(TrailingStopLimitOrder(_aapl, 10, 1000m, 1005m, 60m, trailingAsPercentage: false, 5m));
_tickets.Add(TrailingStopLimitOrder(_aapl, 10, 1000m, 1005m, 0.1m, trailingAsPercentage: true, 5m));
}
}

Expand Down Expand Up @@ -131,6 +133,56 @@ public override void OnEndOfAlgorithm()
}
}
break;

case OrderType.TrailingStopLimit:
stopPrice = ticket.Get(OrderField.StopPrice);
trailingAmount = ticket.Get(OrderField.TrailingAmount);

if (ticket.Get<bool>(OrderField.TrailingAsPercentage))
{
// We only expect one stop price update in this algorithm
if (Math.Abs(stopPrice - _marketPriceAtLatestSplit) > 0.1m * stopPrice)
{
throw new RegressionTestException($"Order with ID: {ticket.OrderId} should have a Stop Price equal to 2.14, but was {stopPrice}");
}

// Trailing amount unchanged since it's a percentage
if (trailingAmount != 0.1m)
{
throw new RegressionTestException($"Order with ID: {ticket.OrderId} should have a Trailing Amount equal to 0.214m, but was {trailingAmount}");
}
}
else
{
// We only expect one stop price update in this algorithm
if (Math.Abs(stopPrice - _marketPriceAtLatestSplit) > 60m * _splitFactor)
{
throw new RegressionTestException($"Order with ID: {ticket.OrderId} should have a Stop Price equal to 2.14, but was {ticket.Get(OrderField.StopPrice)}");
}

if (trailingAmount != 8.57m)
{
throw new RegressionTestException($"Order with ID: {ticket.OrderId} should have a Trailing Amount equal to 8.57m, but was {trailingAmount}");
}
}

// Limit offset should be updated after split
var limitOffset = ticket.Get(OrderField.LimitOffset);
var limitPrice = ticket.Get(OrderField.LimitPrice);
var expectedLimitOffsetAfterSplit = 0.7143m;
var expectedLimitPriceAfterSplit = stopPrice + expectedLimitOffsetAfterSplit;

if (limitOffset != expectedLimitOffsetAfterSplit)
{
throw new RegressionTestException($"Order with ID: {ticket.OrderId} should have a Limit Offset equal to 0.714m, but was {limitOffset}");
}

if (limitPrice != expectedLimitPriceAfterSplit)
{
throw new RegressionTestException($"Order with ID: {ticket.OrderId} should have a Limit Price equal to {expectedLimitPriceAfterSplit}, but was {limitPrice}");
}

break;
}
}
}
Expand Down Expand Up @@ -165,7 +217,7 @@ public override void OnEndOfAlgorithm()
/// </summary>
public Dictionary<string, string> ExpectedStatistics => new Dictionary<string, string>
{
{"Total Orders", "5"},
{"Total Orders", "7"},
{"Average Win", "0%"},
{"Average Loss", "0%"},
{"Compounding Annual Return", "0%"},
Expand All @@ -191,7 +243,7 @@ public override void OnEndOfAlgorithm()
{"Estimated Strategy Capacity", "$0"},
{"Lowest Capacity Asset", ""},
{"Portfolio Turnover", "0%"},
{"OrderListHash", "1433d839e97cd82fc9b051cfd98f166f"}
{"OrderListHash", "db1b4cf6b2280f09a854a785d3c61cbf"}
};
}
}
Loading