Skip to content

Commit e7226f5

Browse files
committed
Regression algorithms
1 parent 97d84df commit e7226f5

6 files changed

+182
-50
lines changed

Algorithm.CSharp/OrderTicketDemoAlgorithm.cs

Lines changed: 22 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -429,11 +429,13 @@ private void TrailingStopOrders()
429429
/// fixed distance from/to the market price, depending on the order direction.
430430
/// The limit price adjusts based on a limit offset compared to the stop price.
431431
/// You can submit requests to update or cancel the StopLimitOrder at any time.
432-
/// The 'StopPrice' or 'LimitPrice' for an order can be retrieved from the ticket
433-
/// using the OrderTicket.Get(OrderField) method, for example:
432+
/// The stop price, trailing amount, limit price and limit offset for an order
433+
/// can be retrieved from the ticket using the OrderTicket.Get(OrderField) method, for example:
434434
/// <code>
435435
/// var currentStopPrice = orderTicket.Get(OrderField.StopPrice);
436+
/// var trailingAmount = orderTicket.Get(OrderField.TrailingAmount);
436437
/// var currentLimitPrice = orderTicket.Get(OrderField.LimitPrice);
438+
/// var limitOffset = orderTicket.Get(OrderField.LimitOffset);
437439
/// </code>
438440
/// </summary>
439441
private void TrailingStopLimitOrders()
@@ -728,33 +730,33 @@ public override void OnEndOfAlgorithm()
728730
/// </summary>
729731
public Dictionary<string, string> ExpectedStatistics => new Dictionary<string, string>
730732
{
731-
{"Total Orders", "12"},
733+
{"Total Orders", "14"},
732734
{"Average Win", "0%"},
733-
{"Average Loss", "-0.01%"},
734-
{"Compounding Annual Return", "77.184%"},
735+
{"Average Loss", "0.00%"},
736+
{"Compounding Annual Return", "63.380%"},
735737
{"Drawdown", "0.100%"},
736738
{"Expectancy", "-1"},
737739
{"Start Equity", "100000"},
738-
{"End Equity", "100734.03"},
739-
{"Net Profit", "0.734%"},
740-
{"Sharpe Ratio", "12.597"},
741-
{"Sortino Ratio", "464.862"},
742-
{"Probabilistic Sharpe Ratio", "99.521%"},
740+
{"End Equity", "100629.62"},
741+
{"Net Profit", "0.630%"},
742+
{"Sharpe Ratio", "12.445"},
743+
{"Sortino Ratio", "680.042"},
744+
{"Probabilistic Sharpe Ratio", "99.827%"},
743745
{"Loss Rate", "100%"},
744746
{"Win Rate", "0%"},
745747
{"Profit-Loss Ratio", "0"},
746-
{"Alpha", "0.2"},
747-
{"Beta", "0.195"},
748-
{"Annual Standard Deviation", "0.047"},
748+
{"Alpha", "0.165"},
749+
{"Beta", "0.161"},
750+
{"Annual Standard Deviation", "0.039"},
749751
{"Annual Variance", "0.002"},
750-
{"Information Ratio", "-7.724"},
751-
{"Tracking Error", "0.18"},
752-
{"Treynor Ratio", "3.002"},
753-
{"Total Fees", "$9.00"},
754-
{"Estimated Strategy Capacity", "$49000000.00"},
752+
{"Information Ratio", "-7.97"},
753+
{"Tracking Error", "0.187"},
754+
{"Treynor Ratio", "2.998"},
755+
{"Total Fees", "$10.00"},
756+
{"Estimated Strategy Capacity", "$51000000.00"},
755757
{"Lowest Capacity Asset", "SPY R735QTJ8XC9X"},
756-
{"Portfolio Turnover", "7.18%"},
757-
{"OrderListHash", "d1ed6571d5895f4c951d287b2903f561"}
758+
{"Portfolio Turnover", "6.90%"},
759+
{"OrderListHash", "4a84e8f5608a8a32ff95d0004a35a822"}
758760
};
759761
}
760762
}

Algorithm.CSharp/SplitEquityRegressionAlgorithm.cs

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ public override void OnData(Slice slice)
5959
_tickets.Add(StopLimitOrder(_aapl, 10, 15, 15));
6060
_tickets.Add(TrailingStopOrder(_aapl, 10, 1000, 60m, trailingAsPercentage: false));
6161
_tickets.Add(TrailingStopOrder(_aapl, 10, 1000, 0.1m, trailingAsPercentage: true));
62+
_tickets.Add(TrailingStopLimitOrder(_aapl, 10, 1000m, 1005m, 60m, trailingAsPercentage: false, 5m));
63+
_tickets.Add(TrailingStopLimitOrder(_aapl, 10, 1000m, 1005m, 0.1m, trailingAsPercentage: true, 5m));
6264
}
6365
}
6466

@@ -131,6 +133,56 @@ public override void OnEndOfAlgorithm()
131133
}
132134
}
133135
break;
136+
137+
case OrderType.TrailingStopLimit:
138+
stopPrice = ticket.Get(OrderField.StopPrice);
139+
trailingAmount = ticket.Get(OrderField.TrailingAmount);
140+
141+
if (ticket.Get<bool>(OrderField.TrailingAsPercentage))
142+
{
143+
// We only expect one stop price update in this algorithm
144+
if (Math.Abs(stopPrice - _marketPriceAtLatestSplit) > 0.1m * stopPrice)
145+
{
146+
throw new RegressionTestException($"Order with ID: {ticket.OrderId} should have a Stop Price equal to 2.14, but was {stopPrice}");
147+
}
148+
149+
// Trailing amount unchanged since it's a percentage
150+
if (trailingAmount != 0.1m)
151+
{
152+
throw new RegressionTestException($"Order with ID: {ticket.OrderId} should have a Trailing Amount equal to 0.214m, but was {trailingAmount}");
153+
}
154+
}
155+
else
156+
{
157+
// We only expect one stop price update in this algorithm
158+
if (Math.Abs(stopPrice - _marketPriceAtLatestSplit) > 60m * _splitFactor)
159+
{
160+
throw new RegressionTestException($"Order with ID: {ticket.OrderId} should have a Stop Price equal to 2.14, but was {ticket.Get(OrderField.StopPrice)}");
161+
}
162+
163+
if (trailingAmount != 8.57m)
164+
{
165+
throw new RegressionTestException($"Order with ID: {ticket.OrderId} should have a Trailing Amount equal to 8.57m, but was {trailingAmount}");
166+
}
167+
}
168+
169+
// Limit offset should be updated after split
170+
var limitOffset = ticket.Get(OrderField.LimitOffset);
171+
var limitPrice = ticket.Get(OrderField.LimitPrice);
172+
var expectedLimitOffsetAfterSplit = 0.7143m;
173+
var expectedLimitPriceAfterSplit = stopPrice + expectedLimitOffsetAfterSplit;
174+
175+
if (limitOffset != expectedLimitOffsetAfterSplit)
176+
{
177+
throw new RegressionTestException($"Order with ID: {ticket.OrderId} should have a Limit Offset equal to 0.714m, but was {limitOffset}");
178+
}
179+
180+
if (limitPrice != expectedLimitPriceAfterSplit)
181+
{
182+
throw new RegressionTestException($"Order with ID: {ticket.OrderId} should have a Limit Price equal to {expectedLimitPriceAfterSplit}, but was {limitPrice}");
183+
}
184+
185+
break;
134186
}
135187
}
136188
}
@@ -165,7 +217,7 @@ public override void OnEndOfAlgorithm()
165217
/// </summary>
166218
public Dictionary<string, string> ExpectedStatistics => new Dictionary<string, string>
167219
{
168-
{"Total Orders", "5"},
220+
{"Total Orders", "7"},
169221
{"Average Win", "0%"},
170222
{"Average Loss", "0%"},
171223
{"Compounding Annual Return", "0%"},
@@ -191,7 +243,7 @@ public override void OnEndOfAlgorithm()
191243
{"Estimated Strategy Capacity", "$0"},
192244
{"Lowest Capacity Asset", ""},
193245
{"Portfolio Turnover", "0%"},
194-
{"OrderListHash", "1433d839e97cd82fc9b051cfd98f166f"}
246+
{"OrderListHash", "db1b4cf6b2280f09a854a785d3c61cbf"}
195247
};
196248
}
197249
}

Algorithm.CSharp/UpdateOrderRegressionAlgorithm.cs

Lines changed: 24 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
using QuantConnect.Securities;
2424
using QuantConnect.Util;
2525
using QuantConnect.Interfaces;
26+
using Accord.MachineLearning.VectorMachines.Learning;
2627

2728
namespace QuantConnect.Algorithm.CSharp
2829
{
@@ -123,8 +124,10 @@ public override void OnData(Slice slice)
123124
Log("TICKET:: " + ticket);
124125
ticket.Update(new UpdateOrderFields
125126
{
126-
LimitPrice = Security.Price*(1 - Math.Sign(ticket.Quantity)*LimitPercentageDelta),
127-
StopPrice = ticket.OrderType != OrderType.TrailingStop
127+
LimitPrice = ticket.OrderType != OrderType.TrailingStopLimit
128+
? Security.Price*(1 - Math.Sign(ticket.Quantity)*LimitPercentageDelta)
129+
: null,
130+
StopPrice = (ticket.OrderType != OrderType.TrailingStop && ticket.OrderType != OrderType.TrailingStopLimit)
128131
? Security.Price*(1 + Math.Sign(ticket.Quantity)*StopPercentageDelta)
129132
: null,
130133
Tag = "Change prices: " + Time.Day
@@ -215,31 +218,31 @@ public override void OnOrderEvent(OrderEvent orderEvent)
215218
{
216219
{"Total Orders", "24"},
217220
{"Average Win", "0%"},
218-
{"Average Loss", "-2.00%"},
219-
{"Compounding Annual Return", "-15.280%"},
220-
{"Drawdown", "30.100%"},
221+
{"Average Loss", "-2.17%"},
222+
{"Compounding Annual Return", "-14.133%"},
223+
{"Drawdown", "28.500%"},
221224
{"Expectancy", "-1"},
222225
{"Start Equity", "100000"},
223-
{"End Equity", "71786.23"},
224-
{"Net Profit", "-28.214%"},
225-
{"Sharpe Ratio", "-1.107"},
226-
{"Sortino Ratio", "-1.357"},
227-
{"Probabilistic Sharpe Ratio", "0.024%"},
226+
{"End Equity", "73741.52"},
227+
{"Net Profit", "-26.258%"},
228+
{"Sharpe Ratio", "-1.072"},
229+
{"Sortino Ratio", "-1.232"},
230+
{"Probabilistic Sharpe Ratio", "0.027%"},
228231
{"Loss Rate", "100%"},
229232
{"Win Rate", "0%"},
230233
{"Profit-Loss Ratio", "0"},
231-
{"Alpha", "0.03"},
232-
{"Beta", "-0.952"},
233-
{"Annual Standard Deviation", "0.1"},
234-
{"Annual Variance", "0.01"},
235-
{"Information Ratio", "-1.375"},
236-
{"Tracking Error", "0.189"},
237-
{"Treynor Ratio", "0.117"},
238-
{"Total Fees", "$20.00"},
239-
{"Estimated Strategy Capacity", "$1000000000.00"},
234+
{"Alpha", "0.031"},
235+
{"Beta", "-0.906"},
236+
{"Annual Standard Deviation", "0.096"},
237+
{"Annual Variance", "0.009"},
238+
{"Information Ratio", "-1.364"},
239+
{"Tracking Error", "0.184"},
240+
{"Treynor Ratio", "0.114"},
241+
{"Total Fees", "$21.00"},
242+
{"Estimated Strategy Capacity", "$750000000.00"},
240243
{"Lowest Capacity Asset", "SPY R735QTJ8XC9X"},
241-
{"Portfolio Turnover", "0.50%"},
242-
{"OrderListHash", "a6482ce8abd669338eaced3104226c1b"}
244+
{"Portfolio Turnover", "0.52%"},
245+
{"OrderListHash", "f2371f5962b956c9d102b95263702242"}
243246
};
244247
}
245248
}

Algorithm.Python/OrderTicketDemoAlgorithm.py

Lines changed: 74 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ def initialize(self):
3939
self.__open_stop_market_orders = []
4040
self.__open_stop_limit_orders = []
4141
self.__open_trailing_stop_orders = []
42-
42+
self.__open_trailing_stop_limit_orders = []
4343

4444
def on_data(self, data):
4545
'''OnData event is the primary entry point for your algorithm. Each new data point will be pumped in here.'''
@@ -64,6 +64,8 @@ def on_data(self, data):
6464
# MARKET ON CLOSE ORDERS
6565
self.market_on_close_orders()
6666

67+
# TRAILING STOP LIMIT ORDERS
68+
self.trailing_stop_limit_orders()
6769

6870
def market_orders(self):
6971
''' MarketOrders are the only orders that are processed synchronously by default, so
@@ -320,6 +322,75 @@ def trailing_stop_orders(self):
320322
update_order_fields.tag = "Update #{0}".format(len(short_order.update_requests) + 1)
321323
short_order.update(update_order_fields)
322324

325+
def trailing_stop_limit_orders(self):
326+
'''TrailingStopLimitOrders work the same way as StopLimitOrders, except
327+
their stop price is adjusted to a certain amount, keeping it a certain
328+
fixed distance from/to the market price, depending on the order direction.
329+
The limit price adjusts based on a limit offset compared to the stop price.
330+
You can submit requests to update or cancel the StopLimitOrder at any time.
331+
The stop price, trailing amount, limit price and limit offset for an order
332+
can be retrieved from the ticket using the OrderTicket.Get(OrderField) method, for example:
333+
Code:
334+
current_stop_price = order_ticket.get(OrderField.STOP_PRICE);
335+
trailing_amount = order_ticket.get(OrderField.TRAILING_AMOUNT);
336+
current_limit_price = order_ticket.get(OrderField.LIMIT_PRICE);
337+
limit_offset = order_ticket.get(OrderField.LIMIT_OFFSET)'''
338+
if self.time_is(7, 12, 0):
339+
self.log("Submitting TrailingStopLimitOrder")
340+
341+
# a long stop is triggered when the price rises above the value
342+
# so we'll set a long stop .25% above the current bar's close
343+
344+
close = self.securities[self.spy.value].close
345+
stop_price = close * 1.0025
346+
limit_price = stop_price + 0.1
347+
new_ticket = self.trailing_stop_limit_order(self.spy.value, 10, stop_price, limit_price,
348+
trailing_amount=0.0025, trailing_as_percentage=True, limit_offset=0.1)
349+
self.__open_trailing_stop_limit_orders.append(new_ticket)
350+
351+
352+
# a short stop is triggered when the price falls below the value
353+
# so we'll set a short stop .25% below the current bar's close
354+
355+
stop_price = close * 0.9975;
356+
limit_price = stop_price - 0.1;
357+
new_ticket = self.trailing_stop_limit_order(self.spy.value, -10, stop_price, limit_price,
358+
trailing_amount=0.0025, trailing_as_percentage=True, limit_offset=0.1);
359+
self.__open_trailing_stop_limit_orders.append(new_ticket)
360+
361+
# when we submitted new trailing stop limit orders we placed them into this list,
362+
# so while there's two entries they're still open and need processing
363+
elif len(self.__open_trailing_stop_limit_orders) == 2:
364+
365+
# check if either is filled and cancel the other
366+
long_order = self.__open_trailing_stop_limit_orders[0]
367+
short_order = self.__open_trailing_stop_limit_orders[1]
368+
if self.check_pair_orders_for_fills(long_order, short_order):
369+
self.__open_trailing_stop_limit_orders = []
370+
return
371+
372+
# if neither order has filled in the last 5 minutes, bring in the trailing percentage by 0.01%
373+
if ((self.utc_time - long_order.time).total_seconds() / 60) % 5 != 0:
374+
return
375+
376+
long_trailing_percentage = long_order.get(OrderField.TRAILING_AMOUNT)
377+
new_long_trailing_percentage = max(long_trailing_percentage - 0.0001, 0.0001)
378+
short_trailing_percentage = short_order.get(OrderField.TRAILING_AMOUNT)
379+
new_short_trailing_percentage = max(short_trailing_percentage - 0.0001, 0.0001)
380+
self.log(self.log("Updating trailing percentages - Long: {0:.3f} Short: {1:.3f}".format(new_long_trailing_percentage, new_short_trailing_percentage)))
381+
382+
update_order_fields = UpdateOrderFields()
383+
# we could change the quantity, but need to specify it
384+
#Quantity =
385+
update_order_fields.trailing_amount = new_long_trailing_percentage
386+
update_order_fields.tag = "Update #{0}".format(len(long_order.update_requests) + 1)
387+
long_order.update(update_order_fields)
388+
389+
update_order_fields = UpdateOrderFields()
390+
update_order_fields.trailing_amount = new_short_trailing_percentage
391+
update_order_fields.tag = "Update #{0}".format(len(short_order.update_requests) + 1)
392+
short_order.update(update_order_fields)
393+
323394

324395
def market_on_close_orders(self):
325396
'''MarketOnCloseOrders are always executed at the next market's closing price.
@@ -439,7 +510,7 @@ def on_end_of_algorithm(self):
439510
order_tickets_size = sum(1 for ticket in order_tickets)
440511
open_order_tickets_size = sum(1 for ticket in open_order_tickets)
441512

442-
assert(filled_orders_size == 9 and order_tickets_size == 12), "There were expected 9 filled orders and 12 order tickets"
513+
assert(filled_orders_size == 10 and order_tickets_size == 14), "There were expected 10 filled orders and 14 order tickets"
443514
assert(not (len(open_orders) or open_order_tickets_size)), "No open orders or tickets were expected"
444515
assert(not remaining_open_orders), "No remaining quantity to be filled from open orders was expected"
445516

@@ -461,6 +532,6 @@ def on_end_of_algorithm(self):
461532
default_order_tickets_size = sum(1 for ticket in default_order_tickets)
462533
default_open_order_tickets_size = sum(1 for ticket in default_open_order_tickets)
463534

464-
assert(default_orders_size == 12 and default_order_tickets_size == 12), "There were expected 12 orders and 12 order tickets"
535+
assert(default_orders_size == 14 and default_order_tickets_size == 14), "There were expected 14 orders and 14 order tickets"
465536
assert(not (len(default_open_orders) or default_open_order_tickets_size)), "No open orders or tickets were expected"
466537
assert(not default_open_orders_remaining), "No remaining quantity to be filled from open orders was expected"

Algorithm.Python/TrailingStopLimitOrderRegressionAlgorithm.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ def on_data(self, slice: Slice):
4848

4949
if not self.is_ready():
5050
return
51-
51+
5252
security = self.securities[self._symbol]
5353

5454
if self._buy_order_ticket is None:

Algorithm.Python/UpdateOrderRegressionAlgorithm.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,9 @@ def initialize(self):
3939
self.limit_percentage = 0.025
4040
self.limit_percentage_delta = 0.005
4141

42-
order_type_enum = [OrderType.MARKET, OrderType.LIMIT, OrderType.STOP_MARKET, OrderType.STOP_LIMIT, OrderType.MARKET_ON_OPEN, OrderType.MARKET_ON_CLOSE, OrderType.TRAILING_STOP]
42+
order_type_enum = [
43+
OrderType.MARKET, OrderType.LIMIT, OrderType.STOP_MARKET, OrderType.STOP_LIMIT, OrderType.MARKET_ON_OPEN,
44+
OrderType.MARKET_ON_CLOSE, OrderType.TRAILING_STOP, OrderType.TRAILING_STOP_LIMIT]
4345
self.order_types_queue = CircularQueue[OrderType](order_type_enum)
4446
self.order_types_queue.circle_completed += self.on_circle_completed
4547
self.tickets = []
@@ -90,8 +92,10 @@ def on_data(self, data):
9092
if len(ticket.update_requests) == 1 and ticket.status is not OrderStatus.FILLED:
9193
self.log("TICKET:: {0}".format(ticket))
9294
update_order_fields = UpdateOrderFields()
93-
update_order_fields.limit_price = self.security.price*(1 - copysign(self.limit_percentage_delta, ticket.quantity))
94-
update_order_fields.stop_price = self.security.price*(1 + copysign(self.stop_percentage_delta, ticket.quantity)) if ticket.order_type != OrderType.TRAILING_STOP else None
95+
update_order_fields.limit_price = self.security.price*(1 - copysign(self.limit_percentage_delta, ticket.quantity)) \
96+
if ticket.order_type != OrderType.TRAILING_STOP_LIMIT else None
97+
update_order_fields.stop_price = self.security.price*(1 + copysign(self.stop_percentage_delta, ticket.quantity)) \
98+
if ticket.order_type not in (OrderType.TRAILING_STOP, OrderType.TRAILING_STOP_LIMIT) else None
9599
update_order_fields.tag = "Change prices: {0}".format(self.time.day)
96100
ticket.update(update_order_fields)
97101
else:

0 commit comments

Comments
 (0)