Skip to content
Draft
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
249 changes: 249 additions & 0 deletions Algorithm.CSharp/OrderSurfaceShortcutsRegressionAlgorithm.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
/*
* QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
* Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

using System;
using System.Collections.Generic;
using System.Linq;

using QuantConnect.Data;
using QuantConnect.Interfaces;
using QuantConnect.Orders;

namespace QuantConnect.Algorithm.CSharp
{
/// <summary>
/// Regression algorithm asserting the flat order surface shortcuts: the numeric OrderFee
/// surface (OrderFee.Amount, arithmetic and comparison operators, OrderEvent.OrderFeeAmount),
/// the flat combo group ids (Order.GroupOrderManagerId, OrderEvent.GroupId, ComboOrderTicket),
/// the tag-argument tolerance of MarketOrder/Liquidate and OrderTargetNotional
/// </summary>
public class OrderSurfaceShortcutsRegressionAlgorithm : QCAlgorithm, IRegressionAlgorithmDefinition
{
private Symbol _equitySymbol;
private Symbol _optionSymbol;
private OrderTicket _taggedTicket;
private OrderTicket _notionalTicket;
private ComboOrderTicket _comboTicket;
private readonly HashSet<int?> _comboFillGroupIds = new();
private int _comboFillEventsCount;

public override void Initialize()
{
SetStartDate(2015, 12, 24);
SetEndDate(2015, 12, 24);
SetCash(200000);

var equity = AddEquity("GOOG", leverage: 4, fillForward: true);
_equitySymbol = equity.Symbol;
var option = AddOption(equity.Symbol, fillForward: true);
_optionSymbol = option.Symbol;

option.SetFilter(u => u.StandardsOnly().Strikes(-2, +2).Expiration(0, 180));
}

public override void OnData(Slice slice)
{
if (_taggedTicket == null && IsMarketOpen(_equitySymbol))
{
// tag in the tag argument slot; the Python version passes it in the third positional slot
_taggedTicket = MarketOrder(_equitySymbol, 1, tag: "tagged entry");

// target an absolute notional value instead of a portfolio percentage
_notionalTicket = OrderTargetNotional(_equitySymbol, 10000);
if (_notionalTicket == null)
{
throw new RegressionTestException("OrderTargetNotional was expected to place an order");
}

// a tag slipped into the symbol slot must fail pointing to the tag parameter
var liquidateFailed = false;
try
{
Liquidate("EOD close");
}
catch (ArgumentException exception)
{
liquidateFailed = true;
if (!exception.Message.Contains("tag"))
{
throw new RegressionTestException(
$"Liquidate() with an unknown ticker was expected to point to the tag parameter but the error was: {exception.Message}");
}
}
if (!liquidateFailed)
{
throw new RegressionTestException("Liquidate() with an unknown ticker was expected to fail");
}
}

if (_comboTicket == null && IsMarketOpen(_optionSymbol) && slice.OptionChains.TryGetValue(_optionSymbol, out var chain))
{
var callContracts = chain.Where(contract => contract.Right == OptionRight.Call)
.GroupBy(x => x.Expiry)
.OrderBy(grouping => grouping.Key)
.First()
.OrderBy(x => x.Strike)
.ToList();
if (callContracts.Count < 3)
{
return;
}

var legs = new List<Leg>()
{
Leg.Create(callContracts[0].Symbol, 1),
Leg.Create(callContracts[1].Symbol, -2),
Leg.Create(callContracts[2].Symbol, 1)
};
_comboTicket = ComboMarketOrder(legs, 10);

if (_comboTicket.Count != legs.Count || _comboTicket.Tickets.Count != legs.Count)
{
throw new RegressionTestException($"Expected {legs.Count} leg tickets, found {_comboTicket.Count}");
}
if (_comboTicket.GroupOrderManagerId == null)
{
throw new RegressionTestException("The combo order ticket was expected to have a group order manager id");
}
}
}

public override void OnOrderEvent(OrderEvent orderEvent)
{
if (orderEvent.Status != OrderStatus.Filled)
{
return;
}

var order = Transactions.GetOrderById(orderEvent.OrderId);

// the fee amount shortcuts and operators must match the two-level Value.Amount
var feeAmount = orderEvent.OrderFee.Value.Amount;
if (orderEvent.OrderFeeAmount != feeAmount || orderEvent.OrderFee.Amount != feeAmount)
{
throw new RegressionTestException($"Order fee amount shortcuts do not match the fee amount {feeAmount}");
}
if (orderEvent.OrderFee + orderEvent.OrderFee != 2 * feeAmount || (feeAmount != 0 && !(orderEvent.OrderFee > 0)))
{
throw new RegressionTestException($"Order fee operators do not match the fee amount {feeAmount}");
}

if (order.Type == OrderType.ComboMarket)
{
// Note: these fill events are received while the synchronous ComboMarketOrder() call is still
// in flight, so the combo ticket is checked against them in OnEndOfAlgorithm
_comboFillEventsCount++;
if (order.GroupOrderManagerId == null)
{
throw new RegressionTestException("Combo orders were expected to have a group order manager id");
}
if (orderEvent.GroupId != order.GroupOrderManagerId)
{
throw new RegressionTestException($"Expected order event group id {order.GroupOrderManagerId}, found {orderEvent.GroupId}");
}
_comboFillGroupIds.Add(order.GroupOrderManagerId);
}
else if (order.GroupOrderManagerId != null || orderEvent.GroupId != null)
{
throw new RegressionTestException("Non-combo orders were expected to have null group ids");
}
}

public override void OnEndOfAlgorithm()
{
if (_taggedTicket == null || _taggedTicket.Tag != "tagged entry")
{
throw new RegressionTestException("The market order tag was not set from the tag argument");
}
if (_notionalTicket.Status != OrderStatus.Filled)
{
throw new RegressionTestException("The notional target order was expected to be filled");
}
if (_comboTicket == null || _comboFillEventsCount != _comboTicket.Count)
{
throw new RegressionTestException("The combo order was expected to be placed and filled");
}
if (!_comboTicket.Filled)
{
throw new RegressionTestException("The combo order ticket was expected to aggregate the leg fills");
}
if (_comboFillGroupIds.Single() != _comboTicket.GroupOrderManagerId)
{
throw new RegressionTestException($"Expected all combo fills to have group id {_comboTicket.GroupOrderManagerId}, " +
$"found {string.Join(", ", _comboFillGroupIds)}");
}
}

/// <summary>
/// This is used by the regression test system to indicate if the open source Lean repository has the required data to run this algorithm.
/// </summary>
public bool CanRunLocally => true;

/// <summary>
/// This is used by the regression test system to indicate which languages this algorithm is written in.
/// </summary>
public List<Language> Languages { get; } = new() { Language.CSharp, Language.Python };

/// <summary>
/// Data Points count of all timeslices of algorithm
/// </summary>
public long DataPoints => 15023;

/// <summary>
/// Data Points count of the algorithm history
/// </summary>
public int AlgorithmHistoryDataPoints => 0;

/// <summary>
/// Final status of the algorithm
/// </summary>
public AlgorithmStatus AlgorithmStatus => AlgorithmStatus.Completed;

/// <summary>
/// This is used by the regression test system to indicate what the expected statistics are from running the algorithm
/// </summary>
public Dictionary<string, string> ExpectedStatistics => new Dictionary<string, string>
{
{"Total Orders", "5"},
{"Average Win", "0%"},
{"Average Loss", "0%"},
{"Compounding Annual Return", "0%"},
{"Drawdown", "0%"},
{"Expectancy", "0"},
{"Start Equity", "200000"},
{"End Equity", "198005.36"},
{"Net Profit", "0%"},
{"Sharpe Ratio", "0"},
{"Sortino Ratio", "0"},
{"Probabilistic Sharpe Ratio", "0%"},
{"Loss Rate", "0%"},
{"Win Rate", "0%"},
{"Profit-Loss Ratio", "0"},
{"Alpha", "0"},
{"Beta", "0"},
{"Annual Standard Deviation", "0"},
{"Annual Variance", "0"},
{"Information Ratio", "0"},
{"Tracking Error", "0"},
{"Treynor Ratio", "0"},
{"Total Fees", "$28.00"},
{"Estimated Strategy Capacity", "$80000.00"},
{"Lowest Capacity Asset", "GOOCV W78ZERHAT67A|GOOCV VP83T1ZUHROL"},
{"Portfolio Turnover", "35.27%"},
{"Drawdown Recovery", "0"},
{"OrderListHash", "94d4e9ad7a0c13884b49d68165ce6766"}
};
}
}
127 changes: 127 additions & 0 deletions Algorithm.Python/OrderSurfaceShortcutsRegressionAlgorithm.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
# QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
# Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from AlgorithmImports import *

### <summary>
### Regression algorithm asserting the flat order surface shortcuts: the numeric OrderFee
### surface (order_fee.amount, arithmetic and comparison operators, order_event.order_fee_amount),
### the flat combo group ids (order.group_order_manager_id, order_event.group_id, ComboOrderTicket),
### the tag-argument tolerance of market_order/liquidate and order_target_notional
### </summary>
class OrderSurfaceShortcutsRegressionAlgorithm(QCAlgorithm):

def initialize(self):
self.set_start_date(2015, 12, 24)
self.set_end_date(2015, 12, 24)
self.set_cash(200000)

equity = self.add_equity("GOOG", leverage=4, fill_forward=True)
self._equity_symbol = equity.symbol
option = self.add_option(equity.symbol, fill_forward=True)
self._option_symbol = option.symbol

option.set_filter(lambda u: u.standards_only().strikes(-2, 2).expiration(0, 180))

self._tagged_ticket = None
self._notional_ticket = None
self._combo_ticket = None
self._combo_fill_group_ids = set()
self._combo_fill_events_count = 0

def on_data(self, slice):
if self._tagged_ticket is None and self.is_market_open(self._equity_symbol):
# the tag in the third positional slot must be accepted as the tag argument
self._tagged_ticket = self.market_order(self._equity_symbol, 1, "tagged entry")

# target an absolute notional value instead of a portfolio percentage
self._notional_ticket = self.order_target_notional(self._equity_symbol, 10000)
if self._notional_ticket is None:
raise AssertionError("order_target_notional was expected to place an order")

# a tag slipped into the symbol slot must fail pointing to the tag parameter
liquidate_failed = False
try:
self.liquidate("EOD close")
except Exception as exception:
liquidate_failed = True
if "tag" not in str(exception):
raise AssertionError("liquidate() with an unknown ticker was expected to point to the "
f"tag parameter but the error was: {exception}")
if not liquidate_failed:
raise AssertionError("liquidate() with an unknown ticker was expected to fail")

if self._combo_ticket is None and self.is_market_open(self._option_symbol):
chain = slice.option_chains.get(self._option_symbol)
if chain is None:
return
call_contracts = [contract for contract in chain if contract.right == OptionRight.CALL]
if not call_contracts:
return
first_expiry = min(contract.expiry for contract in call_contracts)
call_contracts = sorted((contract for contract in call_contracts if contract.expiry == first_expiry),
key=lambda contract: contract.strike)
if len(call_contracts) < 3:
return

legs = [
Leg.create(call_contracts[0].symbol, 1),
Leg.create(call_contracts[1].symbol, -2),
Leg.create(call_contracts[2].symbol, 1),
]
self._combo_ticket = self.combo_market_order(legs, 10)

if len(self._combo_ticket) != len(legs) or len(self._combo_ticket.tickets) != len(legs):
raise AssertionError(f"Expected {len(legs)} leg tickets, found {len(self._combo_ticket)}")
if self._combo_ticket.group_order_manager_id is None:
raise AssertionError("The combo order ticket was expected to have a group order manager id")

def on_order_event(self, order_event):
if order_event.status != OrderStatus.FILLED:
return

order = self.transactions.get_order_by_id(order_event.order_id)

# the fee amount shortcuts and operators must match the two-level value.amount
fee_amount = order_event.order_fee.value.amount
if order_event.order_fee_amount != fee_amount or order_event.order_fee.amount != fee_amount:
raise AssertionError(f"Order fee amount shortcuts do not match the fee amount {fee_amount}")
fee = order_event.order_fee
if fee + fee != 2 * fee_amount or sum([fee, fee], 0) != 2 * fee_amount or (fee_amount != 0 and not fee > 0):
raise AssertionError(f"Order fee operators do not match the fee amount {fee_amount}")

if order.type == OrderType.COMBO_MARKET:
# Note: these fill events are received while the synchronous combo_market_order() call is still
# in flight, so the combo ticket is checked against them in on_end_of_algorithm
self._combo_fill_events_count += 1
if order.group_order_manager_id is None:
raise AssertionError("Combo orders were expected to have a group order manager id")
if order_event.group_id != order.group_order_manager_id:
raise AssertionError(f"Expected order event group id {order.group_order_manager_id}, "
f"found {order_event.group_id}")
self._combo_fill_group_ids.add(order.group_order_manager_id)
elif order.group_order_manager_id is not None or order_event.group_id is not None:
raise AssertionError("Non-combo orders were expected to have null group ids")

def on_end_of_algorithm(self):
if self._tagged_ticket is None or self._tagged_ticket.tag != "tagged entry":
raise AssertionError("The market order tag was not set from the tag argument")
if self._notional_ticket.status != OrderStatus.FILLED:
raise AssertionError("The notional target order was expected to be filled")
if self._combo_ticket is None or self._combo_fill_events_count != len(self._combo_ticket):
raise AssertionError("The combo order was expected to be placed and filled")
if not self._combo_ticket.filled:
raise AssertionError("The combo order ticket was expected to aggregate the leg fills")
if self._combo_fill_group_ids != {self._combo_ticket.group_order_manager_id}:
raise AssertionError(f"Expected all combo fills to have group id {self._combo_ticket.group_order_manager_id}, "
f"found {self._combo_fill_group_ids}")
Loading
Loading