Compare commits

...

1 Commits
17469 ... 17243

Author SHA1 Message Date
Martin Molinero
1d10ec4186 Use ISO for DateTime JsonConverter
- Use ISO for DateTime JsonConverter, keeping backwards compatible.
  Adding unit test
2025-08-08 18:01:02 -03:00
12 changed files with 221 additions and 21 deletions

View File

@@ -75,13 +75,13 @@ namespace QuantConnect
/// <summary>
/// The backtest start date
/// </summary>
[JsonConverter(typeof(DateTimeJsonConverter), DateFormat.UI)]
[JsonConverter(typeof(DateTimeJsonConverter), DateFormat.ISOShort, DateFormat.UI)]
public DateTime StartDate { get; set; }
/// <summary>
/// The backtest end date
/// </summary>
[JsonConverter(typeof(DateTimeJsonConverter), DateFormat.UI)]
[JsonConverter(typeof(DateTimeJsonConverter), DateFormat.ISOShort, DateFormat.UI)]
public DateTime EndDate { get; set; }
/// <summary>

View File

@@ -81,7 +81,7 @@ namespace QuantConnect.Api
/// <summary>
/// Backtest creation date and time
/// </summary>
[JsonConverter(typeof(DateTimeJsonConverter), DateFormat.UI)]
[JsonConverter(typeof(DateTimeJsonConverter), DateFormat.ISOShort, DateFormat.UI)]
public DateTime Created { get; set; }
/// <summary>

View File

@@ -65,7 +65,7 @@ namespace QuantConnect.Api
/// <summary>
/// End date of out of sample data
/// </summary>
[JsonConverter(typeof(DateTimeJsonConverter), DateFormat.UI)]
[JsonConverter(typeof(DateTimeJsonConverter), DateFormat.ISOShort, DateFormat.UI)]
public DateTime? OutOfSampleMaxEndDate { get; set; }
/// <summary>
@@ -87,7 +87,7 @@ namespace QuantConnect.Api
/// <summary>
/// Date when this optimization was created
/// </summary>
[JsonConverter(typeof(DateTimeJsonConverter), DateFormat.UI)]
[JsonConverter(typeof(DateTimeJsonConverter), DateFormat.ISOShort, DateFormat.UI)]
public DateTime Created { get; set; }
/// <summary>

View File

@@ -72,7 +72,7 @@ namespace QuantConnect.Api
/// <summary>
/// Optimization requested date and time
/// </summary>
[JsonConverter(typeof(DateTimeJsonConverter), DateFormat.UI)]
[JsonConverter(typeof(DateTimeJsonConverter), DateFormat.ISOShort, DateFormat.UI)]
public DateTime Requested { get; set; }
}

View File

@@ -76,19 +76,19 @@ namespace QuantConnect.Api
if (optimizationBacktest.StartDate != default)
{
writer.WritePropertyName("startDate");
writer.WriteValue(optimizationBacktest.StartDate.ToStringInvariant(DateFormat.UI));
writer.WriteValue(optimizationBacktest.StartDate.ToStringInvariant(DateFormat.ISOShort));
}
if (optimizationBacktest.EndDate != default)
{
writer.WritePropertyName("endDate");
writer.WriteValue(optimizationBacktest.EndDate.ToStringInvariant(DateFormat.UI));
writer.WriteValue(optimizationBacktest.EndDate.ToStringInvariant(DateFormat.ISOShort));
}
if (optimizationBacktest.OutOfSampleMaxEndDate != null)
{
writer.WritePropertyName("outOfSampleMaxEndDate");
writer.WriteValue(optimizationBacktest.OutOfSampleMaxEndDate.ToStringInvariant(DateFormat.UI));
writer.WriteValue(optimizationBacktest.OutOfSampleMaxEndDate.ToStringInvariant(DateFormat.ISOShort));
writer.WritePropertyName("outOfSampleDays");
writer.WriteValue(optimizationBacktest.OutOfSampleDays);

View File

@@ -41,6 +41,8 @@ namespace QuantConnect
public const string DB = "yyyy-MM-dd HH:mm:ss";
/// QuantConnect UX Date Representation
public const string UI = "yyyy-MM-dd HH:mm:ss";
/// ISO Date Representation
public const string ISOShort = "yyyy-MM-ddTHH:mm:ssZ";
/// en-US Short Date and Time Pattern
public const string USShort = "M/d/yy h:mm tt";
/// en-US Short Date Pattern

View File

@@ -1,4 +1,4 @@
/*
/*
* QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
* Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
*
@@ -14,22 +14,93 @@
*
*/
using System;
using System.Linq;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using System.Collections.Generic;
namespace QuantConnect.Util
{
/// <summary>
/// Provides a json converter that allows defining the date time format used
/// </summary>
public class DateTimeJsonConverter : IsoDateTimeConverter
public class DateTimeJsonConverter : JsonConverter
{
private readonly List<IsoDateTimeConverter> _converters;
/// <summary>
/// Initializes a new instance of the <see cref="DateTimeJsonConverter"/> class
/// True, can read a json into a date time
/// </summary>
/// <param name="format">The date time format</param>
public override bool CanRead => true;
/// <summary>
/// True, can write a datetime to json
/// </summary>
public override bool CanWrite => true;
/// <summary>
/// Initializes a new instance of the <see cref="DateTimeJsonConverter"/> class
/// </summary>
/// <param name="format">>The date time format</param>
public DateTimeJsonConverter(string format)
{
DateTimeFormat = format;
_converters = [new IsoDateTimeConverter() { DateTimeFormat = format }];
}
/// <summary>
/// Initializes a new instance of the <see cref="DateTimeJsonConverter"/> class
/// </summary>
/// <param name="format">>The date time format</param>
/// <param name="format2">Other format for backwards compatibility</param>
public DateTimeJsonConverter(string format, string format2) : this(format)
{
_converters.Add(new IsoDateTimeConverter() { DateTimeFormat = format2 });
}
/// <summary>
/// Initializes a new instance of the <see cref="DateTimeJsonConverter"/> class
/// </summary>
/// <param name="format">>The date time format</param>
/// <param name="format2">Other format for backwards compatibility</param>
/// <param name="format3">Other format for backwards compatibility</param>
public DateTimeJsonConverter(string format, string format2, string format3) : this(format, format2)
{
_converters.Add(new IsoDateTimeConverter() { DateTimeFormat = format3 });
}
/// <summary>
/// True if can convert the given object type
/// </summary>
public override bool CanConvert(Type objectType)
{
return objectType == typeof(DateTime) || objectType == typeof(string) || objectType == typeof(DateTime?);
}
/// <summary>
/// Converts the given value
/// </summary>
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
foreach (var converter in _converters)
{
try
{
return converter.ReadJson(reader, objectType, existingValue, serializer);
}
catch
{
}
}
throw new JsonSerializationException($"Unexpected value when converting date. Expected formats: {string.Join(",", _converters.Select(x => x.DateTimeFormat))}");
}
/// <summary>
/// Writes the given value to json
/// </summary>
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
_converters[0].WriteJson(writer, value, serializer);
}
}
}
}

View File

@@ -313,7 +313,7 @@ namespace QuantConnect.Lean.Engine.Results
ResultsDestinationFolder = Globals.ResultsDestinationFolder;
State = new Dictionary<string, string>
{
["StartTime"] = StartTime.ToStringInvariant(DateFormat.UI),
["StartTime"] = StartTime.ToStringInvariant(DateFormat.ISOShort),
["EndTime"] = string.Empty,
["RuntimeError"] = string.Empty,
["StackTrace"] = string.Empty,
@@ -879,7 +879,7 @@ namespace QuantConnect.Lean.Engine.Results
{
State["Status"] = Algorithm.Status.ToStringInvariant();
}
State["EndTime"] = endTime != null ? endTime.ToStringInvariant(DateFormat.UI) : string.Empty;
State["EndTime"] = endTime != null ? endTime.ToStringInvariant(DateFormat.ISOShort) : string.Empty;
lock (LogStore)
{

View File

@@ -37,7 +37,7 @@ namespace QuantConnect.Optimizer
/// <summary>
/// The creation time
/// </summary>
[JsonConverter(typeof(DateTimeJsonConverter), DateFormat.UI)]
[JsonConverter(typeof(DateTimeJsonConverter), DateFormat.ISOShort, DateFormat.UI)]
public DateTime Created { get; set; }
/// <summary>

View File

@@ -27,7 +27,7 @@ namespace QuantConnect.Tests.API
public class OptimizationBacktestJsonConverterTests
{
private const string _validSerialization = "{\"name\":\"ImABacktestName\",\"id\":\"backtestId\",\"progress\":0.0,\"exitCode\":0," +
"\"startDate\":\"2023-01-01 00:00:00\",\"endDate\":\"2024-01-01 00:00:00\",\"outOfSampleMaxEndDate\":\"2024-01-01 00:00:00\",\"outOfSampleDays\":10,\"statistics\":[0.374,0.217,0.047,-4.51,2.86,-0.664,52.602,17.800,6300000.00,0.196,1.571,27.0,123.888,77.188,0.63,1.707,1390.49,180.0,0.233,-0.558,73.0]," +
"\"startDate\":\"2023-01-01T00:00:00Z\",\"endDate\":\"2024-01-01T00:00:00Z\",\"outOfSampleMaxEndDate\":\"2024-01-01T00:00:00Z\",\"outOfSampleDays\":10,\"statistics\":[0.374,0.217,0.047,-4.51,2.86,-0.664,52.602,17.800,6300000.00,0.196,1.571,27.0,123.888,77.188,0.63,1.707,1390.49,180.0,0.233,-0.558,73.0]," +
"\"parameterSet\":{\"pinocho\":\"19\",\"pepe\":\"-1\"},\"equity\":[[1,1.0,1.0,1.0,1.0],[2,2.0,2.0,2.0,2.0],[3,3.0,3.0,3.0,3.0]]}";
private const string _oldValidSerialization = "{\"name\":\"ImABacktestName\",\"id\":\"backtestId\",\"progress\":0.0,\"exitCode\":0," +
"\"statistics\":[0.374,0.217,0.047,-4.51,2.86,-0.664,52.602,17.800,6300000.00,0.196,1.571,27.0,123.888,77.188,0.63,1.707,1390.49,180.0,0.233,-0.558,73.0]," +

View File

@@ -82,13 +82,13 @@ namespace QuantConnect.Tests.Common
{
serialized = $"{{\"Name\":\"Backtest name\",\"Tags\":[\"tag1\",\"tag2\"],\"AccountCurrency\":\"GBP\",\"Brokerage\":32," +
$"\"AccountType\":1,\"Parameters\":{{\"a\":\"A\",\"b\":\"B\"}},\"OutOfSampleMaxEndDate\":\"2023-01-01T00:00:00\"," +
$"\"OutOfSampleDays\":30,\"StartDate\":\"1998-01-01 00:00:00\",\"EndDate\":\"{algorithm.EndDate.ToString(DateFormat.UI)}\",\"TradingDaysPerYear\":252}}";
$"\"OutOfSampleDays\":30,\"StartDate\":\"1998-01-01T00:00:00Z\",\"EndDate\":\"{algorithm.EndDate.ToString(DateFormat.ISOShort)}\",\"TradingDaysPerYear\":252}}";
}
else
{
Assert.AreEqual($"{{\"name\":\"Backtest name\",\"tags\":[\"tag1\",\"tag2\"],\"accountCurrency\":\"GBP\",\"brokerage\":32," +
$"\"accountType\":1,\"parameters\":{{\"a\":\"A\",\"b\":\"B\"}},\"outOfSampleMaxEndDate\":\"2023-01-01T00:00:00\"," +
$"\"outOfSampleDays\":30,\"startDate\":\"1998-01-01 00:00:00\",\"endDate\":\"{algorithm.EndDate.ToString(DateFormat.UI)}\",\"tradingDaysPerYear\":252}}", serialized);
$"\"outOfSampleDays\":30,\"startDate\":\"1998-01-01T00:00:00Z\",\"endDate\":\"{algorithm.EndDate.ToString(DateFormat.ISOShort)}\",\"tradingDaysPerYear\":252}}", serialized);
}
var deserialize = JsonConvert.DeserializeObject<AlgorithmConfiguration>(serialized);

View File

@@ -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.
*
*/
using System;
using Newtonsoft.Json;
using NUnit.Framework;
using QuantConnect.Util;
namespace QuantConnect.Tests.Common.Util
{
[TestFixture]
public class DateTimeJsonConverterTests
{
[Test]
public void Write()
{
var instance = new MultiDateTimeFormatClassTest { Value = new DateTime(2025, 08, 08, 10, 30, 0) };
var result = JsonConvert.SerializeObject(instance);
Assert.AreEqual("{\"Value\":\"2025-08-08T10:30:00Z\"}", result);
}
[TestCase("{ \"value\": \"2025-08-08 10:30:00\"}", false)]
[TestCase("{ \"value\": \"2025-08-08T10:30:00Z\"}", false)]
[TestCase("{ \"value\": \"2025-08-08T10:30:00.000Z\"}", false)]
[TestCase("{ \"value\": \"20250808-10:30:00\"}", false)]
[TestCase("{ \"value\": \"2025/08/08 10:30:00.000\"}", true)]
[TestCase("{ \"value\": \"2025-08-08 10:30:00.000\"}", true)]
public void MultipleFormats(string strObject, bool throws)
{
if (throws)
{
Assert.Throws<JsonSerializationException>(() => JsonConvert.DeserializeObject<SingleDateTimeFormatClassTest>(strObject));
return;
}
var result = JsonConvert.DeserializeObject<MultiDateTimeFormatClassTest>(strObject);
Assert.AreEqual(new DateTime(2025, 08, 08, 10, 30, 0), result.Value);
}
[TestCase("{ \"value\": \"2025-08-08 10:30:00\"}", false)]
[TestCase("{ \"value\": \"2025-08-08T10:30:00\"}", false)]
[TestCase("{ \"value\": \"2025-08-08T10:30:00Z\"}", false)]
[TestCase("{ \"value\": \"2025/08/08 10:30:00\"}", true)]
public void SingleFormat(string strObject, bool throws)
{
if (throws)
{
Assert.Throws<JsonSerializationException>(() => JsonConvert.DeserializeObject<SingleDateTimeFormatClassTest>(strObject));
return;
}
var result = JsonConvert.DeserializeObject<SingleDateTimeFormatClassTest>(strObject);
Assert.AreEqual(new DateTime(2025, 08, 08, 10, 30, 0), result.Value);
}
[TestCase("{ \"value\": \"2025-08-08T10:30:00\"}", false, false)]
[TestCase("{ \"value\": \"2025-08-08T10:30:00Z\"}", false, false)]
[TestCase("{ \"value\": null}", false, true)]
[TestCase("{ }", false, true)]
[TestCase("{ \"value\": \"2025/08/08 10:30:00\"}", true, false)]
[TestCase("{ \"value\": \"2025-08-08 10:30:00\"}", true, false)]
public void NullFormat(string strObject, bool throws, bool expectNull)
{
if (throws)
{
Assert.Throws<JsonSerializationException>(() => JsonConvert.DeserializeObject<NullableDateTimeFormatClassTest>(strObject));
return;
}
var result = JsonConvert.DeserializeObject<NullableDateTimeFormatClassTest>(strObject);
if (expectNull)
{
Assert.AreEqual(null, result.Value);
return;
}
Assert.AreEqual(new DateTime(2025, 08, 08, 10, 30, 0), result.Value);
}
[TestCase("\"2025-08-08T10:30:00Z\"", false)]
[TestCase("\"2025-08-08 10:30:00\"", false)]
[TestCase("", true)]
[TestCase("\"\"", true)]
public void ManualConversion(string strObject, bool expectNull)
{
var converter = new DateTimeJsonConverter(DateFormat.ISOShort, DateFormat.UI);
var result = JsonConvert.DeserializeObject<DateTime?>(strObject, converter);
if (expectNull)
{
Assert.AreEqual(null, result);
return;
}
Assert.AreEqual(new DateTime(2025, 08, 08, 10, 30, 0), result.Value);
}
internal class MultiDateTimeFormatClassTest
{
[JsonConverter(typeof(DateTimeJsonConverter), DateFormat.ISOShort, DateFormat.UI, DateFormat.FIX)]
public DateTime Value { get; set; }
}
internal class SingleDateTimeFormatClassTest
{
[JsonConverter(typeof(DateTimeJsonConverter), DateFormat.UI)]
public DateTime Value { get; set; }
}
internal class NullableDateTimeFormatClassTest
{
[JsonConverter(typeof(DateTimeJsonConverter), DateFormat.ISOShort)]
public DateTime? Value { get; set; }
}
}
}