Skip to content

Commit 1dd742d

Browse files
Order sizing fix
- Improving OrderSizing.Value and Volume to include code in consumers - OrderSizing.GetUnorderedQuantity() will adjust result by lot size - ImmediateExecutionModels will use OrderSizing.GetUnorderedQuantity() - OrderSizing.Value() will take into account ContractMultiplier - Adding unit tests
1 parent 47f6d5b commit 1dd742d

10 files changed

Lines changed: 241 additions & 65 deletions

Algorithm.Framework/Execution/StandardDeviationExecutionModel.cs

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -93,15 +93,12 @@ public override void Execute(QCAlgorithm algorithm, IPortfolioTarget[] targets)
9393
// check order entry conditions
9494
if (data.STD.IsReady && PriceIsFavorable(data, unorderedQuantity))
9595
{
96-
// get the maximum order size based on total order value
97-
var maxOrderSize = OrderSizing.Value(data.Security, MaximumOrderValue);
98-
var orderSize = Math.Min(maxOrderSize, Math.Abs(unorderedQuantity));
96+
// Adjust order size to respect the maximum total order value
97+
var orderSize = OrderSizing.GetOrderSizeForMaximumValue(data.Security, MaximumOrderValue, unorderedQuantity);
9998

100-
// round down to even lot size
101-
orderSize -= orderSize % data.Security.SymbolProperties.LotSize;
10299
if (orderSize != 0)
103100
{
104-
algorithm.MarketOrder(symbol, Math.Sign(unorderedQuantity) * orderSize);
101+
algorithm.MarketOrder(symbol, orderSize);
105102
}
106103
}
107104
}

Algorithm.Framework/Execution/StandardDeviationExecutionModel.py

Lines changed: 4 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -78,22 +78,11 @@ def Execute(self, algorithm, targets):
7878

7979
# check order entry conditions
8080
if data.STD.IsReady and self.PriceIsFavorable(data, unorderedQuantity):
81-
# get the maximum order size based on total order value
82-
maxOrderSize = OrderSizing.Value(data.Security, self.MaximumOrderValue)
83-
orderSize = np.min([maxOrderSize, np.abs(unorderedQuantity)])
84-
85-
remainder = orderSize % data.Security.SymbolProperties.LotSize
86-
missingForLotSize = data.Security.SymbolProperties.LotSize - remainder
87-
# if the amount we are missing for +1 lot size is 1M part of a lot size
88-
# we suppose its due to floating point error and round up
89-
# Note: this is required to avoid a diff with C# equivalent
90-
if missingForLotSize < (data.Security.SymbolProperties.LotSize / 1000000):
91-
remainder -= data.Security.SymbolProperties.LotSize
92-
93-
# round down to even lot size
94-
orderSize -= remainder
81+
# Adjust order size to respect the maximum total order value
82+
orderSize = OrderSizing.GetOrderSizeForMaximumValue(data.Security, self.MaximumOrderValue, unorderedQuantity)
83+
9584
if orderSize != 0:
96-
algorithm.MarketOrder(symbol, np.sign(unorderedQuantity) * orderSize)
85+
algorithm.MarketOrder(symbol, orderSize)
9786

9887
self.targetsCollection.ClearFulfilled(algorithm)
9988

Algorithm.Framework/Execution/VolumeWeightedAveragePriceExecutionModel.cs

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -72,15 +72,13 @@ public override void Execute(QCAlgorithm algorithm, IPortfolioTarget[] targets)
7272
// check order entry conditions
7373
if (PriceIsFavorable(data, unorderedQuantity))
7474
{
75-
// get the maximum order size based on a percentage of current volume
76-
var maxOrderSize = OrderSizing.PercentVolume(data.Security, MaximumOrderQuantityPercentVolume);
77-
var orderSize = Math.Min(maxOrderSize, Math.Abs(unorderedQuantity));
75+
// adjust order size to respect maximum order size based on a percentage of current volume
76+
var orderSize = OrderSizing.GetOrderSizeForPercentVolume(
77+
data.Security, MaximumOrderQuantityPercentVolume, unorderedQuantity);
7878

79-
// round down to even lot size
80-
orderSize -= orderSize % data.Security.SymbolProperties.LotSize;
8179
if (orderSize != 0)
8280
{
83-
algorithm.MarketOrder(data.Security.Symbol, Math.Sign(unorderedQuantity) * orderSize);
81+
algorithm.MarketOrder(data.Security.Symbol, orderSize);
8482
}
8583
}
8684
}

Algorithm.Framework/Execution/VolumeWeightedAveragePriceExecutionModel.py

Lines changed: 4 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -70,22 +70,11 @@ def Execute(self, algorithm, targets):
7070

7171
# check order entry conditions
7272
if self.PriceIsFavorable(data, unorderedQuantity):
73-
# get the maximum order size based on total order value
74-
maxOrderSize = OrderSizing.PercentVolume(data.Security, self.MaximumOrderQuantityPercentVolume)
75-
orderSize = np.min([maxOrderSize, np.abs(unorderedQuantity)])
76-
77-
remainder = orderSize % data.Security.SymbolProperties.LotSize
78-
missingForLotSize = data.Security.SymbolProperties.LotSize - remainder
79-
# if the amount we are missing for +1 lot size is 1M part of a lot size
80-
# we suppose its due to floating point error and round up
81-
# Note: this is required to avoid a diff with C# equivalent
82-
if missingForLotSize < (data.Security.SymbolProperties.LotSize / 1000000):
83-
remainder -= data.Security.SymbolProperties.LotSize
84-
85-
# round down to even lot size
86-
orderSize -= remainder
73+
# adjust order size to respect maximum order size based on a percentage of current volume
74+
orderSize = OrderSizing.GetOrderSizeForPercentVolume(data.Security, self.MaximumOrderQuantityPercentVolume, unorderedQuantity)
75+
8776
if orderSize != 0:
88-
algorithm.MarketOrder(symbol, np.sign(unorderedQuantity) * orderSize)
77+
algorithm.MarketOrder(symbol, orderSize)
8978

9079
self.targetsCollection.ClearFulfilled(algorithm)
9180

Algorithm/Execution/ImmediateExecutionModel.cs

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@
1313
* limitations under the License.
1414
*/
1515

16-
using System.Linq;
1716
using QuantConnect.Algorithm.Framework.Portfolio;
1817
using QuantConnect.Data.UniverseSelection;
18+
using QuantConnect.Orders;
1919

2020
namespace QuantConnect.Algorithm.Framework.Execution
2121
{
@@ -40,10 +40,8 @@ public override void Execute(QCAlgorithm algorithm, IPortfolioTarget[] targets)
4040
{
4141
foreach (var target in _targetsCollection.OrderByMarginImpact(algorithm))
4242
{
43-
var existing = algorithm.Securities[target.Symbol].Holdings.Quantity
44-
+ algorithm.Transactions.GetOpenOrderTickets(target.Symbol)
45-
.Aggregate(0m, (d, ticket) => d + ticket.Quantity - ticket.QuantityFilled);
46-
var quantity = target.Quantity - existing;
43+
// calculate remaining quantity to be ordered
44+
var quantity = OrderSizing.GetUnorderedQuantity(algorithm, target);
4745
if (quantity != 0)
4846
{
4947
algorithm.MarketOrder(target.Symbol, quantity);

Algorithm/Execution/ImmediateExecutionModel.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,8 @@ def Execute(self, algorithm, targets):
4242
self.targetsCollection.AddRange(targets)
4343
if self.targetsCollection.Count > 0:
4444
for target in self.targetsCollection.OrderByMarginImpact(algorithm):
45-
open_quantity = sum([x.Quantity - x.QuantityFilled for x in algorithm.Transactions.GetOpenOrderTickets(target.Symbol)])
46-
existing = algorithm.Securities[target.Symbol].Holdings.Quantity + open_quantity
47-
quantity = target.Quantity - existing
45+
# calculate remaining quantity to be ordered
46+
quantity = OrderSizing.GetUnorderedQuantity(algorithm, target)
4847
if quantity != 0:
4948
algorithm.MarketOrder(target.Symbol, quantity)
5049

Common/Orders/OrderSizing.cs

Lines changed: 44 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -27,40 +27,50 @@ namespace QuantConnect.Orders
2727
public static class OrderSizing
2828
{
2929
/// <summary>
30-
/// Gets the maximum order size as a percentage of the current bar's volume.
30+
/// Adjust the provided order size to respect maximum order size based on a percentage of current volume.
3131
/// </summary>
3232
/// <param name="security">The security object</param>
3333
/// <param name="maximumPercentCurrentVolume">The maximum percentage of the current bar's volume</param>
34-
/// <returns>The fractional quantity of shares that equal the specified percentage of the current bar's volume</returns>
35-
public static decimal PercentVolume(Security security, decimal maximumPercentCurrentVolume)
34+
/// <param name="desiredOrderSize">The desired order size to adjust</param>
35+
/// <returns>The signed adjusted order size</returns>
36+
public static decimal GetOrderSizeForPercentVolume(Security security, decimal maximumPercentCurrentVolume, decimal desiredOrderSize)
3637
{
37-
return maximumPercentCurrentVolume * security.Volume;
38+
var maxOrderSize = maximumPercentCurrentVolume * security.Volume;
39+
var orderSize = Math.Min(maxOrderSize, Math.Abs(desiredOrderSize));
40+
41+
return Math.Sign(desiredOrderSize) * AdjustByLotSize(security, orderSize);
3842
}
3943

4044
/// <summary>
41-
/// Gets the maximum order size using a maximum order value in units of the account currency
45+
/// Adjust the provided order size to respect the maximum total order value
4246
/// </summary>
4347
/// <param name="security">The security object</param>
4448
/// <param name="maximumOrderValueInAccountCurrency">The maximum order value in units of the account currency</param>
45-
/// <returns>The quantity of fractional of shares that yield the specified maximum order value</returns>
46-
public static decimal Value(Security security, decimal maximumOrderValueInAccountCurrency)
49+
/// <param name="desiredOrderSize">The desired order size to adjust</param>
50+
/// <returns>The signed adjusted order size</returns>
51+
public static decimal GetOrderSizeForMaximumValue(Security security, decimal maximumOrderValueInAccountCurrency, decimal desiredOrderSize)
4752
{
48-
var priceInAccountCurrency = security.Price * security.QuoteCurrency.ConversionRate;
53+
var priceInAccountCurrency = security.Price
54+
* security.QuoteCurrency.ConversionRate
55+
* security.SymbolProperties.ContractMultiplier;
4956

5057
if (priceInAccountCurrency == 0m)
5158
{
5259
return 0m;
5360
}
5461

55-
return maximumOrderValueInAccountCurrency / priceInAccountCurrency;
62+
var maxOrderSize = maximumOrderValueInAccountCurrency / priceInAccountCurrency;
63+
var orderSize = Math.Min(maxOrderSize, Math.Abs(desiredOrderSize));
64+
65+
return Math.Sign(desiredOrderSize) * AdjustByLotSize(security, orderSize);
5666
}
5767

5868
/// <summary>
5969
/// Gets the remaining quantity to be ordered to reach the specified target quantity.
6070
/// </summary>
6171
/// <param name="algorithm">The algorithm instance</param>
6272
/// <param name="target">The portfolio target</param>
63-
/// <returns>The remaining quantity to be ordered</returns>
73+
/// <returns>The signed remaining quantity to be ordered</returns>
6474
public static decimal GetUnorderedQuantity(IAlgorithm algorithm, IPortfolioTarget target)
6575
{
6676
var security = algorithm.Securities[target.Symbol];
@@ -69,13 +79,33 @@ public static decimal GetUnorderedQuantity(IAlgorithm algorithm, IPortfolioTarge
6979
.Aggregate(0m, (d, t) => d + t.Quantity - t.QuantityFilled);
7080
var quantity = target.Quantity - holdings - openOrderQuantity;
7181

72-
// check if we're below the lot size threshold
73-
if (Math.Abs(quantity) < security.SymbolProperties.LotSize)
82+
return AdjustByLotSize(security, quantity);
83+
}
84+
85+
/// <summary>
86+
/// Adjusts the provided order quantity to respect the securities lot size.
87+
/// If the quantity is missing 1M part of the lot size it will be rounded up
88+
/// since we suppose it's due to floating point error, this is required to avoid diff
89+
/// between Py and C#
90+
/// </summary>
91+
/// <param name="security">The security instance</param>
92+
/// <param name="quantity">The desired quantity to adjust, can be signed</param>
93+
/// <returns>The signed adjusted quantity</returns>
94+
public static decimal AdjustByLotSize(Security security, decimal quantity)
95+
{
96+
var absQuantity = Math.Abs(quantity);
97+
// if the amount we are missing for +1 lot size is 1M part of a lot size
98+
// we suppose its due to floating point error and round up
99+
// Note: this is required to avoid a diff between Py and C# equivalent
100+
var remainder = absQuantity % security.SymbolProperties.LotSize;
101+
var missingForLotSize = security.SymbolProperties.LotSize - remainder;
102+
if (missingForLotSize < (security.SymbolProperties.LotSize / 1000000))
74103
{
75-
return 0m;
104+
remainder -= security.SymbolProperties.LotSize;
76105
}
106+
absQuantity -= remainder;
77107

78-
return quantity;
108+
return absQuantity * Math.Sign(quantity);
79109
}
80110
}
81111
}

Tests/Algorithm/Framework/Execution/ImmediateExecutionModelTests.cs

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ public void OrdersAreSubmittedImmediatelyForTargetsToExecute(
7979
var time = new DateTime(2018, 8, 2, 16, 0, 0);
8080
var historyProvider = new Mock<IHistoryProvider>();
8181
historyProvider.Setup(m => m.GetHistory(It.IsAny<IEnumerable<HistoryRequest>>(), It.IsAny<DateTimeZone>()))
82-
.Returns(historicalPrices.Select((x,i) =>
82+
.Returns(historicalPrices.Select((x, i) =>
8383
new Slice(time.AddMinutes(i),
8484
new List<BaseData>
8585
{
@@ -186,6 +186,40 @@ public void PartiallyFilledOrdersAreTakenIntoAccount(Language language)
186186
Assert.AreEqual(50, actualOrdersSubmitted.Sum(x => x.Quantity));
187187
}
188188

189+
[TestCase(Language.CSharp, -1)]
190+
[TestCase(Language.Python, -1)]
191+
[TestCase(Language.CSharp, 1)]
192+
[TestCase(Language.Python, 1)]
193+
public void LotSizeIsRespected(Language language, int side)
194+
{
195+
var actualOrdersSubmitted = new List<SubmitOrderRequest>();
196+
197+
var algorithm = new QCAlgorithm();
198+
algorithm.SubscriptionManager.SetDataManager(new DataManagerStub(algorithm));
199+
algorithm.SetPandasConverter();
200+
201+
var security = algorithm.AddForex(Symbols.EURUSD.Value);
202+
algorithm.Portfolio.SetCash("EUR", 1, 1);
203+
security.SetMarketPrice(new TradeBar { Value = 250 });
204+
205+
algorithm.SetFinishedWarmingUp();
206+
207+
var orderProcessor = new Mock<IOrderProcessor>();
208+
orderProcessor.Setup(m => m.Process(It.IsAny<SubmitOrderRequest>()))
209+
.Returns((SubmitOrderRequest request) => new OrderTicket(algorithm.Transactions, request))
210+
.Callback((SubmitOrderRequest request) => actualOrdersSubmitted.Add(request));
211+
algorithm.Transactions.SetOrderProcessor(orderProcessor.Object);
212+
213+
var model = GetExecutionModel(language);
214+
algorithm.SetExecution(model);
215+
216+
model.Execute(algorithm,
217+
new IPortfolioTarget[] { new PortfolioTarget(Symbols.EURUSD, security.SymbolProperties.LotSize * 1.5m * side) });
218+
219+
Assert.AreEqual(1, actualOrdersSubmitted.Count);
220+
Assert.AreEqual(security.SymbolProperties.LotSize * side, actualOrdersSubmitted.Single().Quantity);
221+
}
222+
189223
private static IExecutionModel GetExecutionModel(Language language)
190224
{
191225
if (language == Language.Python)

0 commit comments

Comments
 (0)