Skip to content

Commit 99e04e7

Browse files
committed
feat: oco implementation
1 parent c6ef320 commit 99e04e7

32 files changed

Lines changed: 2254 additions & 8 deletions
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
/*
2+
* QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
3+
* Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*/
15+
16+
using System;
17+
using System.Collections.Generic;
18+
using System.Linq;
19+
using QuantConnect.Data;
20+
using QuantConnect.Interfaces;
21+
using QuantConnect.Orders;
22+
23+
namespace QuantConnect.Algorithm.CSharp
24+
{
25+
/// <summary>
26+
/// Regression algorithm for the cancel path of a one-cancels-the-other (OCO) order group: the group is
27+
/// placed with both legs far from the market, so neither can fill inside the test window, then one of the
28+
/// two tickets is explicitly canceled. Asserts that canceling one leg cancels the whole group, not just
29+
/// the leg that was canceled
30+
/// </summary>
31+
public class OneCancelsTheOtherOrderCancelRegressionAlgorithm : QCAlgorithm, IRegressionAlgorithmDefinition
32+
{
33+
private Symbol _spy;
34+
private List<OrderTicket> _tickets;
35+
private bool _canceled;
36+
37+
public override void Initialize()
38+
{
39+
SetStartDate(2019, 1, 1);
40+
SetEndDate(2019, 1, 31);
41+
42+
_spy = AddEquity("SPY", Resolution.Hour).Symbol;
43+
}
44+
45+
public override void OnData(Slice slice)
46+
{
47+
if (!Portfolio.Invested)
48+
{
49+
MarketOrder(_spy, 100);
50+
51+
// both legs sit far from the market: limit sell +30% and stop sell -30% should never be
52+
// reachable in this test window, so only the explicit cancel below can close the group
53+
_tickets = OneCancelsTheOtherOrder(new List<Order>
54+
{
55+
new LimitOrder(_spy, -100, Math.Round(Securities[_spy].Price * 1.30m, 2), UtcTime),
56+
new StopMarketOrder(_spy, -100, Math.Round(Securities[_spy].Price * 0.70m, 2), UtcTime)
57+
});
58+
}
59+
else if (!_canceled && Time.Day > 5)
60+
{
61+
// cancel only one leg: the whole OCO group must cancel with it
62+
_tickets[0].Cancel();
63+
_canceled = true;
64+
}
65+
}
66+
67+
public override void OnOrderEvent(OrderEvent orderEvent)
68+
{
69+
if (_tickets == null || orderEvent.Status != OrderStatus.Filled)
70+
{
71+
return;
72+
}
73+
74+
// neither OCO leg's price should ever be reachable in this test window; a fill here means the
75+
// regression scenario itself is broken, not just the cancellation behavior being tested
76+
if (_tickets.Any(ticket => ticket.OrderId == orderEvent.OrderId))
77+
{
78+
throw new RegressionTestException(
79+
$"Unexpected fill for OCO leg {orderEvent.OrderId}: prices were set far from the market so the group should only close through the explicit cancel");
80+
}
81+
}
82+
83+
public override void OnEndOfAlgorithm()
84+
{
85+
if (!_canceled)
86+
{
87+
throw new RegressionTestException("Expected to have canceled one of the OCO legs before the end of the algorithm");
88+
}
89+
90+
if (_tickets == null || _tickets.Count != 2)
91+
{
92+
throw new RegressionTestException("Expected the OCO group to have exactly 2 legs");
93+
}
94+
95+
foreach (var ticket in _tickets)
96+
{
97+
if (ticket.Status != OrderStatus.Canceled)
98+
{
99+
throw new RegressionTestException(
100+
$"Expected every OCO leg to be Canceled, including the leg that was not explicitly canceled. Leg {ticket.OrderId} has status {ticket.Status}");
101+
}
102+
}
103+
104+
// canceling the OCO exit group must not touch the original market order fill
105+
if (!Portfolio.Invested)
106+
{
107+
throw new RegressionTestException("Expected the algorithm to still be invested: the market order fill is independent from the canceled OCO group");
108+
}
109+
}
110+
111+
/// <summary>
112+
/// This is used by the regression test system to indicate if the open source Lean repository has the required data to run this algorithm.
113+
/// </summary>
114+
public bool CanRunLocally { get; } = true;
115+
116+
/// <summary>
117+
/// This is used by the regression test system to indicate which languages this algorithm is written in.
118+
/// </summary>
119+
public List<Language> Languages { get; } = new() { Language.CSharp, Language.Python };
120+
121+
/// <summary>
122+
/// Data Points count of all timeslices of algorithm
123+
/// </summary>
124+
public long DataPoints => 302;
125+
126+
/// <summary>
127+
/// Data Points count of the algorithm history
128+
/// </summary>
129+
public int AlgorithmHistoryDataPoints => 0;
130+
131+
/// <summary>
132+
/// Final status of the algorithm
133+
/// </summary>
134+
public AlgorithmStatus AlgorithmStatus => AlgorithmStatus.Completed;
135+
136+
/// <summary>
137+
/// This is used by the regression test system to indicate what the expected statistics are from running the algorithm
138+
/// </summary>
139+
public Dictionary<string, string> ExpectedStatistics => new Dictionary<string, string>
140+
{
141+
{"Total Orders", "3"},
142+
{"Average Win", "0%"},
143+
{"Average Loss", "0%"},
144+
{"Compounding Annual Return", "29.303%"},
145+
{"Drawdown", "0.700%"},
146+
{"Expectancy", "0"},
147+
{"Start Equity", "100000"},
148+
{"End Equity", "102182.68"},
149+
{"Net Profit", "2.183%"},
150+
{"Sharpe Ratio", "4.501"},
151+
{"Sortino Ratio", "5.158"},
152+
{"Probabilistic Sharpe Ratio", "85.073%"},
153+
{"Loss Rate", "0%"},
154+
{"Win Rate", "0%"},
155+
{"Profit-Loss Ratio", "0"},
156+
{"Alpha", "-0.047"},
157+
{"Beta", "0.24"},
158+
{"Annual Standard Deviation", "0.038"},
159+
{"Annual Variance", "0.001"},
160+
{"Information Ratio", "-6.241"},
161+
{"Tracking Error", "0.117"},
162+
{"Treynor Ratio", "0.708"},
163+
{"Total Fees", "$1.00"},
164+
{"Estimated Strategy Capacity", "$470000000.00"},
165+
{"Lowest Capacity Asset", "SPY R735QTJ8XC9X"},
166+
{"Portfolio Turnover", "0.76%"},
167+
{"Drawdown Recovery", "12"},
168+
{"OrderListHash", "eed60d3f37058f4f436ee819cb6228d7"}
169+
};
170+
}
171+
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
/*
2+
* QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
3+
* Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*/
15+
16+
using System;
17+
using QuantConnect.Data;
18+
using QuantConnect.Orders;
19+
using System.Collections.Generic;
20+
21+
namespace QuantConnect.Algorithm.CSharp
22+
{
23+
/// <summary>
24+
/// Demo algorithm for manually testing one-cancels-the-other (OCO) order groups end to end against a live
25+
/// or paper brokerage. Alpaca is the first brokerage to support them - see
26+
/// Lean.Brokerages.Alpaca/Documentation/ADR/0001-one-cancels-the-other-orders.md for the brokerage-side design.
27+
/// Buys a small position at market, then places a 2-leg OCO exit (take-profit limit above the entry price,
28+
/// stop-loss below it) and logs every order event so the outcome is visible in the live log/console.
29+
/// </summary>
30+
/// <remarks>
31+
/// This is a manual/live testing aid, not part of the automated backtest regression suite - it deliberately
32+
/// does not implement IRegressionAlgorithmDefinition. For the automated backtest version of this scenario,
33+
/// see OneCancelsTheOtherOrderRegressionAlgorithm.
34+
/// </remarks>
35+
public class OneCancelsTheOtherOrderDemoAlgorithm : QCAlgorithm
36+
{
37+
private Symbol _symbol;
38+
private List<OrderTicket> _tickets;
39+
40+
public override void Initialize()
41+
{
42+
// ignored when deployed live; only used if this is run as a quick local backtest sanity check first
43+
SetStartDate(2019, 1, 1);
44+
SetEndDate(2019, 1, 31);
45+
SetCash(100000);
46+
47+
_symbol = AddEquity("AAPL", Resolution.Minute).Symbol;
48+
}
49+
50+
public override void OnData(Slice slice)
51+
{
52+
if (_tickets != null)
53+
{
54+
// the OCO exit group has already been placed, nothing left to do
55+
return;
56+
}
57+
58+
if (!Portfolio.Invested)
59+
{
60+
Debug("Buying 10 AAPL at market to open the position the OCO exit group will close.");
61+
MarketOrder(_symbol, 10);
62+
return;
63+
}
64+
65+
// just went long: place the exit as one OCO group (take profit +1%, stop loss -2% from here).
66+
// Tighten these offsets if you want one leg to trigger quickly for a faster manual test.
67+
var price = Securities[_symbol].Price;
68+
var takeProfitLimitPrice = Math.Round(price * 1.01m, 2);
69+
var stopLossStopPrice = Math.Round(price * 0.98m, 2);
70+
71+
Debug($"Placing OCO exit group on {_symbol}: sell limit {takeProfitLimitPrice} (take profit) / sell stop {stopLossStopPrice} (stop loss)");
72+
73+
_tickets = OneCancelsTheOtherOrder(
74+
[
75+
new LimitOrder(_symbol, -10, takeProfitLimitPrice, UtcTime),
76+
new StopMarketOrder(_symbol, -10, stopLossStopPrice, UtcTime)
77+
]);
78+
}
79+
80+
public override void OnOrderEvent(OrderEvent orderEvent)
81+
{
82+
Debug($"{Time}: {orderEvent}");
83+
}
84+
85+
public override void OnEndOfAlgorithm()
86+
{
87+
if (_tickets == null)
88+
{
89+
Debug("OCO exit group was never placed.");
90+
return;
91+
}
92+
93+
foreach (var ticket in _tickets)
94+
{
95+
Debug($"Final status - Order {ticket.OrderId} ({ticket.OrderType}): {ticket.Status}");
96+
}
97+
}
98+
}
99+
}

0 commit comments

Comments
 (0)