Skip to content

Commit

Permalink
Regression algorithms
Browse files Browse the repository at this point in the history
  • Loading branch information
wtindall1 committed Nov 10, 2024
1 parent 97d84df commit e7226f5
Show file tree
Hide file tree
Showing 6 changed files with 182 additions and 50 deletions.
42 changes: 22 additions & 20 deletions Algorithm.CSharp/OrderTicketDemoAlgorithm.cs
Original file line number Diff line number Diff line change
Expand Up @@ -429,11 +429,13 @@ private void TrailingStopOrders()
/// 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 'StopPrice' or 'LimitPrice' for an order can be retrieved from the ticket
/// using the OrderTicket.Get(OrderField) method, for example:
/// 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()
Expand Down Expand Up @@ -728,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"}
};
}
}
45 changes: 24 additions & 21 deletions Algorithm.CSharp/UpdateOrderRegressionAlgorithm.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
using QuantConnect.Securities;
using QuantConnect.Util;
using QuantConnect.Interfaces;
using Accord.MachineLearning.VectorMachines.Learning;

namespace QuantConnect.Algorithm.CSharp
{
Expand Down Expand Up @@ -123,8 +124,10 @@ public override void OnData(Slice slice)
Log("TICKET:: " + ticket);
ticket.Update(new UpdateOrderFields
{
LimitPrice = Security.Price*(1 - Math.Sign(ticket.Quantity)*LimitPercentageDelta),
StopPrice = ticket.OrderType != OrderType.TrailingStop
LimitPrice = ticket.OrderType != OrderType.TrailingStopLimit
? Security.Price*(1 - Math.Sign(ticket.Quantity)*LimitPercentageDelta)
: null,
StopPrice = (ticket.OrderType != OrderType.TrailingStop && ticket.OrderType != OrderType.TrailingStopLimit)
? Security.Price*(1 + Math.Sign(ticket.Quantity)*StopPercentageDelta)
: null,
Tag = "Change prices: " + Time.Day
Expand Down Expand Up @@ -215,31 +218,31 @@ public override void OnOrderEvent(OrderEvent orderEvent)
{
{"Total Orders", "24"},
{"Average Win", "0%"},
{"Average Loss", "-2.00%"},
{"Compounding Annual Return", "-15.280%"},
{"Drawdown", "30.100%"},
{"Average Loss", "-2.17%"},
{"Compounding Annual Return", "-14.133%"},
{"Drawdown", "28.500%"},
{"Expectancy", "-1"},
{"Start Equity", "100000"},
{"End Equity", "71786.23"},
{"Net Profit", "-28.214%"},
{"Sharpe Ratio", "-1.107"},
{"Sortino Ratio", "-1.357"},
{"Probabilistic Sharpe Ratio", "0.024%"},
{"End Equity", "73741.52"},
{"Net Profit", "-26.258%"},
{"Sharpe Ratio", "-1.072"},
{"Sortino Ratio", "-1.232"},
{"Probabilistic Sharpe Ratio", "0.027%"},
{"Loss Rate", "100%"},
{"Win Rate", "0%"},
{"Profit-Loss Ratio", "0"},
{"Alpha", "0.03"},
{"Beta", "-0.952"},
{"Annual Standard Deviation", "0.1"},
{"Annual Variance", "0.01"},
{"Information Ratio", "-1.375"},
{"Tracking Error", "0.189"},
{"Treynor Ratio", "0.117"},
{"Total Fees", "$20.00"},
{"Estimated Strategy Capacity", "$1000000000.00"},
{"Alpha", "0.031"},
{"Beta", "-0.906"},
{"Annual Standard Deviation", "0.096"},
{"Annual Variance", "0.009"},
{"Information Ratio", "-1.364"},
{"Tracking Error", "0.184"},
{"Treynor Ratio", "0.114"},
{"Total Fees", "$21.00"},
{"Estimated Strategy Capacity", "$750000000.00"},
{"Lowest Capacity Asset", "SPY R735QTJ8XC9X"},
{"Portfolio Turnover", "0.50%"},
{"OrderListHash", "a6482ce8abd669338eaced3104226c1b"}
{"Portfolio Turnover", "0.52%"},
{"OrderListHash", "f2371f5962b956c9d102b95263702242"}
};
}
}
77 changes: 74 additions & 3 deletions Algorithm.Python/OrderTicketDemoAlgorithm.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ def initialize(self):
self.__open_stop_market_orders = []
self.__open_stop_limit_orders = []
self.__open_trailing_stop_orders = []

self.__open_trailing_stop_limit_orders = []

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

# TRAILING STOP LIMIT ORDERS
self.trailing_stop_limit_orders()

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

def trailing_stop_limit_orders(self):
'''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:
current_stop_price = order_ticket.get(OrderField.STOP_PRICE);
trailing_amount = order_ticket.get(OrderField.TRAILING_AMOUNT);
current_limit_price = order_ticket.get(OrderField.LIMIT_PRICE);
limit_offset = order_ticket.get(OrderField.LIMIT_OFFSET)'''
if self.time_is(7, 12, 0):
self.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

close = self.securities[self.spy.value].close
stop_price = close * 1.0025
limit_price = stop_price + 0.1
new_ticket = self.trailing_stop_limit_order(self.spy.value, 10, stop_price, limit_price,
trailing_amount=0.0025, trailing_as_percentage=True, limit_offset=0.1)
self.__open_trailing_stop_limit_orders.append(new_ticket)


# 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

stop_price = close * 0.9975;
limit_price = stop_price - 0.1;
new_ticket = self.trailing_stop_limit_order(self.spy.value, -10, stop_price, limit_price,
trailing_amount=0.0025, trailing_as_percentage=True, limit_offset=0.1);
self.__open_trailing_stop_limit_orders.append(new_ticket)

# 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
elif len(self.__open_trailing_stop_limit_orders) == 2:

# check if either is filled and cancel the other
long_order = self.__open_trailing_stop_limit_orders[0]
short_order = self.__open_trailing_stop_limit_orders[1]
if self.check_pair_orders_for_fills(long_order, short_order):
self.__open_trailing_stop_limit_orders = []
return

# if neither order has filled in the last 5 minutes, bring in the trailing percentage by 0.01%
if ((self.utc_time - long_order.time).total_seconds() / 60) % 5 != 0:
return

long_trailing_percentage = long_order.get(OrderField.TRAILING_AMOUNT)
new_long_trailing_percentage = max(long_trailing_percentage - 0.0001, 0.0001)
short_trailing_percentage = short_order.get(OrderField.TRAILING_AMOUNT)
new_short_trailing_percentage = max(short_trailing_percentage - 0.0001, 0.0001)
self.log(self.log("Updating trailing percentages - Long: {0:.3f} Short: {1:.3f}".format(new_long_trailing_percentage, new_short_trailing_percentage)))

update_order_fields = UpdateOrderFields()
# we could change the quantity, but need to specify it
#Quantity =
update_order_fields.trailing_amount = new_long_trailing_percentage
update_order_fields.tag = "Update #{0}".format(len(long_order.update_requests) + 1)
long_order.update(update_order_fields)

update_order_fields = UpdateOrderFields()
update_order_fields.trailing_amount = new_short_trailing_percentage
update_order_fields.tag = "Update #{0}".format(len(short_order.update_requests) + 1)
short_order.update(update_order_fields)


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

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

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

assert(default_orders_size == 12 and default_order_tickets_size == 12), "There were expected 12 orders and 12 order tickets"
assert(default_orders_size == 14 and default_order_tickets_size == 14), "There were expected 14 orders and 14 order tickets"
assert(not (len(default_open_orders) or default_open_order_tickets_size)), "No open orders or tickets were expected"
assert(not default_open_orders_remaining), "No remaining quantity to be filled from open orders was expected"
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ def on_data(self, slice: Slice):

if not self.is_ready():
return

security = self.securities[self._symbol]

if self._buy_order_ticket is None:
Expand Down
10 changes: 7 additions & 3 deletions Algorithm.Python/UpdateOrderRegressionAlgorithm.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@ def initialize(self):
self.limit_percentage = 0.025
self.limit_percentage_delta = 0.005

order_type_enum = [OrderType.MARKET, OrderType.LIMIT, OrderType.STOP_MARKET, OrderType.STOP_LIMIT, OrderType.MARKET_ON_OPEN, OrderType.MARKET_ON_CLOSE, OrderType.TRAILING_STOP]
order_type_enum = [
OrderType.MARKET, OrderType.LIMIT, OrderType.STOP_MARKET, OrderType.STOP_LIMIT, OrderType.MARKET_ON_OPEN,
OrderType.MARKET_ON_CLOSE, OrderType.TRAILING_STOP, OrderType.TRAILING_STOP_LIMIT]
self.order_types_queue = CircularQueue[OrderType](order_type_enum)
self.order_types_queue.circle_completed += self.on_circle_completed
self.tickets = []
Expand Down Expand Up @@ -90,8 +92,10 @@ def on_data(self, data):
if len(ticket.update_requests) == 1 and ticket.status is not OrderStatus.FILLED:
self.log("TICKET:: {0}".format(ticket))
update_order_fields = UpdateOrderFields()
update_order_fields.limit_price = self.security.price*(1 - copysign(self.limit_percentage_delta, ticket.quantity))
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
update_order_fields.limit_price = self.security.price*(1 - copysign(self.limit_percentage_delta, ticket.quantity)) \
if ticket.order_type != OrderType.TRAILING_STOP_LIMIT else None
update_order_fields.stop_price = self.security.price*(1 + copysign(self.stop_percentage_delta, ticket.quantity)) \
if ticket.order_type not in (OrderType.TRAILING_STOP, OrderType.TRAILING_STOP_LIMIT) else None
update_order_fields.tag = "Change prices: {0}".format(self.time.day)
ticket.update(update_order_fields)
else:
Expand Down

0 comments on commit e7226f5

Please sign in to comment.