Skip to content

Commit

Permalink
Fixes issue when parsing LiveResult JSON with string OrderType values
Browse files Browse the repository at this point in the history
  • Loading branch information
gsalaz98 committed Apr 11, 2020
1 parent 8a98910 commit 093ddc8
Show file tree
Hide file tree
Showing 5 changed files with 237 additions and 7 deletions.
17 changes: 17 additions & 0 deletions Common/Extensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,23 @@ public static string ToCamelCase(this string value)
return char.ToLowerInvariant(value[0]) + value.Substring(1);
}

/// <summary>
/// Converts the provided string into pascal case notation
/// </summary>
public static string ToPascalCase(this string value)
{
if (string.IsNullOrEmpty(value))
{
return value;
}

if (value.Length == 1)
{
return value.ToUpperInvariant();
}
return char.ToUpperInvariant(value[0]) + value.Substring(1);
}

/// <summary>
/// Helper method to batch a collection of <see cref="AlphaResultPacket"/> into 1 single instance.
/// Will return null if the provided list is empty. Will keep the last Order instance per order id,
Expand Down
2 changes: 1 addition & 1 deletion Report/NullResultValueTypeJsonConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ public override object ReadJson(JsonReader reader, Type objectType, object exist
// Deserialize with OrderJsonConverter, otherwise it will fail. We convert the token back
// to its JSON representation and use the `JsonConvert.DeserializeObject<T>(...)` method instead
// of using `token.ToObject<T>()` since it can be provided a JsonConverter in its arguments.
return JsonConvert.DeserializeObject<T>(token.ToString(), new Orders.OrderJsonConverter());
return JsonConvert.DeserializeObject<T>(token.ToString(), new OrderTypeNormalizingJsonConverter());
}

public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
Expand Down
111 changes: 111 additions & 0 deletions Report/OrderTypeNormalizingJsonConverter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
/*
* 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.Globalization;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using QuantConnect.Orders;

namespace QuantConnect.Report
{
/// <summary>
/// Normalizes the "Type" field to a value that will allow for
/// successful deserialization in the <see cref="OrderJsonConverter"/> class.
/// </summary>
/// <example>
/// All of these values should result in the same object:
/// <code>
/// [
/// { "Type": "marketOnOpen", ... },
/// { "Type": "MarketOnOpen", ... },
/// { "Type": 4, ... },
/// ]
/// </code>
/// </example>
/// <typeparam name="T">Result type to deserialize into</typeparam>
public class OrderTypeNormalizingJsonConverter : JsonConverter
{
private readonly JsonConverter _converter;

/// <summary>
/// Creates an instance of the class
/// </summary>
public OrderTypeNormalizingJsonConverter()
{
_converter = new OrderJsonConverter();
}

public override bool CanConvert(Type objectType)
{
return typeof(Order).IsAssignableFrom(objectType);
}

public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var token = JToken.ReadFrom(reader);
// Takes the Type field and selects the correct OrderType instance
var orderTypeValue = token["Type"].Value<string>();
int orderTypeNumber;
var orderType = Parse.TryParse(orderTypeValue, NumberStyles.Any, out orderTypeNumber) ?
(OrderType)orderTypeNumber :
(OrderType)Enum.Parse(typeof(OrderType), orderTypeValue.ToPascalCase());

var typeOfOrder = TypeFromOrderTypeEnum(orderType);
token["Type"] = (int)orderType;
return JsonConvert.DeserializeObject(token.ToString(), typeOfOrder, _converter);
}

public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}

/// <summary>
/// Returns the object type belonging to the provided OrderType
/// </summary>
/// <param name="orderType">Order type</param>
/// <returns>Class type that supports the given OrderType</returns>
public static Type TypeFromOrderTypeEnum(OrderType orderType)
{
switch (orderType)
{
case OrderType.Market:
return typeof(MarketOrder);

case OrderType.Limit:
return typeof(LimitOrder);

case OrderType.StopMarket:
return typeof(StopMarketOrder);

case OrderType.StopLimit:
return typeof(StopLimitOrder);

case OrderType.MarketOnOpen:
return typeof(MarketOnOpenOrder);

case OrderType.MarketOnClose:
return typeof(MarketOnCloseOrder);

case OrderType.OptionExercise:
return typeof(OptionExerciseOrder);

default:
throw new ArgumentOutOfRangeException();
}
}
}
}
1 change: 1 addition & 0 deletions Report/QuantConnect.Report.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@
<Compile Include="..\Common\Properties\SharedAssemblyInfo.cs">
<Link>Properties\SharedAssemblyInfo.cs</Link>
</Compile>
<Compile Include="OrderTypeNormalizingJsonConverter.cs" />
<Compile Include="NullResultValueTypeJsonConverter.cs" />
<Compile Include="PortfolioLooper\MockDataFeed.cs" />
<Compile Include="PortfolioLooper\PortfolioLooperAlgorithm.cs" />
Expand Down
113 changes: 107 additions & 6 deletions Tests/Report/ResultDeserializationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@

using Newtonsoft.Json;
using NUnit.Framework;
using QuantConnect.Orders;
using QuantConnect.Packets;
using QuantConnect.Report;
using System;
using System.Collections.Generic;
using System.Linq;

Expand All @@ -25,8 +27,31 @@ namespace QuantConnect.Tests.Report
[TestFixture]
public class ResultDeserializationTests
{
public const string InvalidBacktestResultJson = "{\"RollingWindow\":{},\"TotalPerformance\":null,\"Charts\":{\"Equity\":{\"Name\":\"Equity\",\"ChartType\":0,\"Series\":{\"Performance\":{\"Name\":\"Performance\",\"Unit\":\"$\",\"Index\":0,\"Values\":[{\"x\":1583704925,\"y\":5.0},{\"x\":1583791325,\"y\":null},{\"x\":1583877725,\"y\":7.0},{\"x\":1583964125,\"y\":8.0},{\"x\":1584050525,\"y\":9.0}],\"SeriesType\":0,\"Color\":\"\",\"ScatterMarkerSymbol\":\"none\"}}}},\"Orders\":{},\"ProfitLoss\":{},\"Statistics\":{},\"RuntimeStatistics\":{}}";
public const string InvalidLiveResultJson = "{\"Holdings\":{},\"Cash\":{\"USD\":{\"SecuritySymbol\":{\"Value\":\"\",\"ID\":\" 0\",\"Permtick\":\"\"},\"Symbol\":\"USD\",\"Amount\":0.0,\"ConversionRate\":1.0,\"CurrencySymbol\":\"$\",\"ValueInAccountCurrency\":0.0}},\"ServerStatistics\":{\"CPU Usage\":\"0.0%\",\"Used RAM (MB)\":\"68\",\"Total RAM (MB)\":\"\",\"Used Disk Space (MB)\":\"1\",\"Total Disk Space (MB)\":\"5\",\"Hostname\":\"LEAN\",\"LEAN Version\":\"v2.4.0.0\"},\"Charts\":{\"Equity\":{\"Name\":\"Equity\",\"ChartType\":0,\"Series\":{\"Performance\":{\"Name\":\"Performance\",\"Unit\":\"$\",\"Index\":0,\"Values\":[{\"x\":1583705127,\"y\":5.0},{\"x\":1583791527,\"y\":null},{\"x\":1583877927,\"y\":7.0},{\"x\":1583964327,\"y\":8.0},{\"x\":1584050727,\"y\":9.0}],\"SeriesType\":0,\"Color\":\"\",\"ScatterMarkerSymbol\":\"none\"}}}},\"Orders\":{},\"ProfitLoss\":{},\"Statistics\":{},\"RuntimeStatistics\":{}}";
public const string OrderStringReplace = "{{orderStringReplace}}";
public const string OrderTypeStringReplace = "{{marketOrderType}}";
public const string EmptyJson = "{}";

public const string InvalidBacktestResultJson = "{\"RollingWindow\":{},\"TotalPerformance\":null,\"Charts\":{\"Equity\":{\"Name\":\"Equity\",\"ChartType\":0,\"Series\":{\"Performance\":{\"Name\":\"Performance\",\"Unit\":\"$\",\"Index\":0,\"Values\":[{\"x\":1583704925,\"y\":5.0},{\"x\":1583791325,\"y\":null},{\"x\":1583877725,\"y\":7.0},{\"x\":1583964125,\"y\":8.0},{\"x\":1584050525,\"y\":9.0}],\"SeriesType\":0,\"Color\":\"\",\"ScatterMarkerSymbol\":\"none\"}}}},\"Orders\":" + OrderStringReplace + ",\"ProfitLoss\":{},\"Statistics\":{},\"RuntimeStatistics\":{}}";
public const string InvalidLiveResultJson = "{\"Holdings\":{},\"Cash\":{\"USD\":{\"SecuritySymbol\":{\"Value\":\"\",\"ID\":\" 0\",\"Permtick\":\"\"},\"Symbol\":\"USD\",\"Amount\":0.0,\"ConversionRate\":1.0,\"CurrencySymbol\":\"$\",\"ValueInAccountCurrency\":0.0}},\"ServerStatistics\":{\"CPU Usage\":\"0.0%\",\"Used RAM (MB)\":\"68\",\"Total RAM (MB)\":\"\",\"Used Disk Space (MB)\":\"1\",\"Total Disk Space (MB)\":\"5\",\"Hostname\":\"LEAN\",\"LEAN Version\":\"v2.4.0.0\"},\"Charts\":{\"Equity\":{\"Name\":\"Equity\",\"ChartType\":0,\"Series\":{\"Performance\":{\"Name\":\"Performance\",\"Unit\":\"$\",\"Index\":0,\"Values\":[{\"x\":1583705127,\"y\":5.0},{\"x\":1583791527,\"y\":null},{\"x\":1583877927,\"y\":7.0},{\"x\":1583964327,\"y\":8.0},{\"x\":1584050727,\"y\":9.0}],\"SeriesType\":0,\"Color\":\"\",\"ScatterMarkerSymbol\":\"none\"}}}},\"Orders\":" + OrderStringReplace + ",\"ProfitLoss\":{},\"Statistics\":{},\"RuntimeStatistics\":{}}";
public const string OrderJson = @"{'1': {
'Type':" + OrderTypeStringReplace + @",
'Value':99986.827413672,
'Id':1,
'ContingentId':0,
'BrokerId':[1],
'Symbol':{'Value':'SPY',
'Permtick':'SPY'},
'Price':100.086914328,
'Time':'2010-03-04T14:31:00Z',
'Quantity':999,
'Status':3,
'Duration':2,
'DurationValue':'2010-04-04T14:31:00Z',
'Tag':'',
'SecurityType':1,
'Direction':0,
'AbsoluteQuantity':999
}}";

[Test]
public void BacktestResult_NullChartPoint_IsSkipped()
Expand All @@ -37,8 +62,8 @@ public void BacktestResult_NullChartPoint_IsSkipped()
NullValueHandling = NullValueHandling.Ignore
};

var deWithoutConverter = JsonConvert.DeserializeObject<BacktestResult>(InvalidBacktestResultJson, settings);
var deWithConverter = JsonConvert.DeserializeObject<BacktestResult>(InvalidBacktestResultJson, converter);
var deWithoutConverter = JsonConvert.DeserializeObject<BacktestResult>(InvalidBacktestResultJson.Replace(OrderStringReplace, EmptyJson), settings);
var deWithConverter = JsonConvert.DeserializeObject<BacktestResult>(InvalidBacktestResultJson.Replace(OrderStringReplace, EmptyJson), converter);

var noConverterPoints = GetChartPoints(deWithoutConverter).ToList();
var withConverterPoints = GetChartPoints(deWithConverter).ToList();
Expand All @@ -61,8 +86,8 @@ public void LiveResult_NullChartPoint_IsSkipped()
NullValueHandling = NullValueHandling.Ignore
};

var deWithoutConverter = JsonConvert.DeserializeObject<LiveResult>(InvalidLiveResultJson, settings);
var deWithConverter = JsonConvert.DeserializeObject<LiveResult>(InvalidLiveResultJson, converter);
var deWithoutConverter = JsonConvert.DeserializeObject<LiveResult>(InvalidLiveResultJson.Replace(OrderStringReplace, EmptyJson), settings);
var deWithConverter = JsonConvert.DeserializeObject<LiveResult>(InvalidLiveResultJson.Replace(OrderStringReplace, EmptyJson), converter);

var noConverterPoints = GetChartPoints(deWithoutConverter).ToList();
var withConverterPoints = GetChartPoints(deWithConverter).ToList();
Expand All @@ -76,6 +101,82 @@ public void LiveResult_NullChartPoint_IsSkipped()
Assert.IsTrue(withConverterPoints.SequenceEqual(GetChartPoints(roundtripDeserialization).ToList()));
}

[Test]
public void OrderTypeEnumStringAndValueDeserialization()
{

var settings = new JsonSerializerSettings
{
Converters = new[] { new NullResultValueTypeJsonConverter<LiveResult>() }
};

foreach (var orderType in (OrderType[])Enum.GetValues(typeof(OrderType)))
{
//var orderObjectType = OrderTypeNormalizingJsonConverter.TypeFromOrderTypeEnum(orderType);
var intValueJson = OrderJson.Replace(OrderTypeStringReplace, ((int)orderType).ToStringInvariant());
var pascalCaseJson = OrderJson.Replace(OrderTypeStringReplace, $"'{orderType.ToStringInvariant().ToPascalCase()}'");
var camelCaseJson = OrderJson.Replace(OrderTypeStringReplace, $"'{orderType.ToStringInvariant().ToCamelCase()}'");

var intValueLiveResult = InvalidLiveResultJson.Replace(OrderStringReplace, intValueJson);
var pascalCaseLiveResult = InvalidLiveResultJson.Replace(OrderStringReplace, pascalCaseJson);
var camelCaseLiveResult = InvalidLiveResultJson.Replace(OrderStringReplace, camelCaseJson);

var intInstance = JsonConvert.DeserializeObject<LiveResult>(intValueLiveResult, settings).Orders.Values.Single();
var pascalCaseInstance = JsonConvert.DeserializeObject<LiveResult>(pascalCaseLiveResult, settings).Orders.Values.Single();
var camelCaseInstance = JsonConvert.DeserializeObject<LiveResult>(camelCaseLiveResult, settings).Orders.Values.Single();

CollectionAssert.AreEqual(intInstance.BrokerId, pascalCaseInstance.BrokerId);
Assert.AreEqual(intInstance.ContingentId, pascalCaseInstance.ContingentId);
Assert.AreEqual(intInstance.Direction, pascalCaseInstance.Direction);
Assert.AreEqual(intInstance.TimeInForce.GetType(), pascalCaseInstance.TimeInForce.GetType());
Assert.AreEqual(intInstance.Id, pascalCaseInstance.Id);
Assert.AreEqual(intInstance.Price, pascalCaseInstance.Price);
Assert.AreEqual(intInstance.PriceCurrency, pascalCaseInstance.PriceCurrency);
Assert.AreEqual(intInstance.SecurityType, pascalCaseInstance.SecurityType);
Assert.AreEqual(intInstance.Status, pascalCaseInstance.Status);
Assert.AreEqual(intInstance.Symbol, pascalCaseInstance.Symbol);
Assert.AreEqual(intInstance.Tag, pascalCaseInstance.Tag);
Assert.AreEqual(intInstance.Time, pascalCaseInstance.Time);
Assert.AreEqual(intInstance.CreatedTime, pascalCaseInstance.CreatedTime);
Assert.AreEqual(intInstance.LastFillTime, pascalCaseInstance.LastFillTime);
Assert.AreEqual(intInstance.LastUpdateTime, pascalCaseInstance.LastUpdateTime);
Assert.AreEqual(intInstance.CanceledTime, pascalCaseInstance.CanceledTime);
Assert.AreEqual(intInstance.Type, pascalCaseInstance.Type);
Assert.AreEqual(intInstance.Value, pascalCaseInstance.Value);
Assert.AreEqual(intInstance.Quantity, pascalCaseInstance.Quantity);
Assert.AreEqual(intInstance.TimeInForce.GetType(), pascalCaseInstance.TimeInForce.GetType());
Assert.AreEqual(intInstance.Symbol.ID.Market, pascalCaseInstance.Symbol.ID.Market);
Assert.AreEqual(intInstance.OrderSubmissionData?.AskPrice, pascalCaseInstance.OrderSubmissionData?.AskPrice);
Assert.AreEqual(intInstance.OrderSubmissionData?.BidPrice, pascalCaseInstance.OrderSubmissionData?.BidPrice);
Assert.AreEqual(intInstance.OrderSubmissionData?.LastPrice, pascalCaseInstance.OrderSubmissionData?.LastPrice);

CollectionAssert.AreEqual(intInstance.BrokerId, camelCaseInstance.BrokerId);
Assert.AreEqual(intInstance.ContingentId, camelCaseInstance.ContingentId);
Assert.AreEqual(intInstance.Direction, camelCaseInstance.Direction);
Assert.AreEqual(intInstance.TimeInForce.GetType(), camelCaseInstance.TimeInForce.GetType());
Assert.AreEqual(intInstance.Id, camelCaseInstance.Id);
Assert.AreEqual(intInstance.Price, camelCaseInstance.Price);
Assert.AreEqual(intInstance.PriceCurrency, camelCaseInstance.PriceCurrency);
Assert.AreEqual(intInstance.SecurityType, camelCaseInstance.SecurityType);
Assert.AreEqual(intInstance.Status, camelCaseInstance.Status);
Assert.AreEqual(intInstance.Symbol, camelCaseInstance.Symbol);
Assert.AreEqual(intInstance.Tag, camelCaseInstance.Tag);
Assert.AreEqual(intInstance.Time, camelCaseInstance.Time);
Assert.AreEqual(intInstance.CreatedTime, camelCaseInstance.CreatedTime);
Assert.AreEqual(intInstance.LastFillTime, camelCaseInstance.LastFillTime);
Assert.AreEqual(intInstance.LastUpdateTime, camelCaseInstance.LastUpdateTime);
Assert.AreEqual(intInstance.CanceledTime, camelCaseInstance.CanceledTime);
Assert.AreEqual(intInstance.Type, camelCaseInstance.Type);
Assert.AreEqual(intInstance.Value, camelCaseInstance.Value);
Assert.AreEqual(intInstance.Quantity, camelCaseInstance.Quantity);
Assert.AreEqual(intInstance.TimeInForce.GetType(), camelCaseInstance.TimeInForce.GetType());
Assert.AreEqual(intInstance.Symbol.ID.Market, camelCaseInstance.Symbol.ID.Market);
Assert.AreEqual(intInstance.OrderSubmissionData?.AskPrice, camelCaseInstance.OrderSubmissionData?.AskPrice);
Assert.AreEqual(intInstance.OrderSubmissionData?.BidPrice, camelCaseInstance.OrderSubmissionData?.BidPrice);
Assert.AreEqual(intInstance.OrderSubmissionData?.LastPrice, camelCaseInstance.OrderSubmissionData?.LastPrice);
}
}

public IEnumerable<KeyValuePair<long, decimal>> GetChartPoints(Result result)
{
return result.Charts["Equity"].Series["Performance"].Values.Select(point => new KeyValuePair<long, decimal>(point.x, point.y));
Expand Down

0 comments on commit 093ddc8

Please sign in to comment.