Skip to content

Commit

Permalink
Cleanup and tests
Browse files Browse the repository at this point in the history
  • Loading branch information
jhonabreul committed Sep 4, 2024
1 parent 2beb276 commit 3327576
Show file tree
Hide file tree
Showing 4 changed files with 230 additions and 40 deletions.
38 changes: 16 additions & 22 deletions Common/Data/UniverseSelection/OptionUniverse.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ namespace QuantConnect.Data.UniverseSelection
/// </summary>
public class OptionUniverse : BaseDataCollection, ISymbol
{
private bool _throwIfNotAnOption = true;
private const int StartingGreeksCsvIndex = 7;

// We keep the properties as they are in the csv file to reduce memory usage (strings vs decimals)
private readonly string _csvLine;

Expand Down Expand Up @@ -129,8 +130,7 @@ public BaseGreeks Greeks
get
{
ThrowIfNotAnOption(nameof(Greeks));

return new PrecalculatedGreeks(_csvLine, 7);
return new PreCalculatedGreeks(_csvLine);
}
}

Expand Down Expand Up @@ -279,15 +279,11 @@ public override BaseData Clone()
/// <summary>
/// Gets the CSV string representation of this universe entry
/// </summary>
public string ToCsv()
public static string ToCsv(Symbol symbol, decimal open, decimal high, decimal low, decimal close, decimal volume, decimal? openInterest,
decimal? impliedVolatility, BaseGreeks greeks)
{
_throwIfNotAnOption = false;
// Single access to avoid parsing the csv multiple times
var greeks = Greeks;
var csv = $"{Symbol.ID},{Symbol.Value},{Open},{High},{Low},{Close},{Volume}," +
$"{OpenInterest},{ImpliedVolatility},{greeks.Delta},{greeks.Gamma},{greeks.Vega},{greeks.Theta},{greeks.Rho}";
_throwIfNotAnOption = true;
return csv;
return $"{symbol.ID},{symbol.Value},{open},{high},{low},{close},{volume},"
+ $"{openInterest},{impliedVolatility},{greeks?.Delta},{greeks?.Gamma},{greeks?.Vega},{greeks?.Theta},{greeks?.Rho}";
}

/// <summary>
Expand Down Expand Up @@ -315,7 +311,7 @@ public Symbol ToSymbol()
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void ThrowIfNotAnOption(string propertyName)
{
if (_throwIfNotAnOption && !Symbol.SecurityType.IsOption())
if (!Symbol.SecurityType.IsOption())
{
throw new InvalidOperationException($"{propertyName} is only available for options.");
}
Expand All @@ -325,43 +321,42 @@ private void ThrowIfNotAnOption(string propertyName)
/// Pre-calculated greeks lazily parsed from csv line.
/// It parses the greeks values from the csv line only when they are requested to avoid holding decimals in memory.
/// </summary>
public class PrecalculatedGreeks : BaseGreeks
private class PreCalculatedGreeks : BaseGreeks
{
private readonly string _csvLine;
private readonly int _startingIndex;

/// <inheritdoc />
public override decimal Delta
{
get => _csvLine.GetDecimalFromCsv(_startingIndex);
get => _csvLine.GetDecimalFromCsv(StartingGreeksCsvIndex);
protected set => throw new InvalidOperationException("Delta is read-only.");
}

/// <inheritdoc />
public override decimal Gamma
{
get => _csvLine.GetDecimalFromCsv(_startingIndex + 1);
get => _csvLine.GetDecimalFromCsv(StartingGreeksCsvIndex + 1);
protected set => throw new InvalidOperationException("Gamma is read-only.");
}

/// <inheritdoc />
public override decimal Vega
{
get => _csvLine.GetDecimalFromCsv(_startingIndex + 2);
get => _csvLine.GetDecimalFromCsv(StartingGreeksCsvIndex + 2);
protected set => throw new InvalidOperationException("Vega is read-only.");
}

/// <inheritdoc />
public override decimal Theta
{
get => _csvLine.GetDecimalFromCsv(_startingIndex + 3);
get => _csvLine.GetDecimalFromCsv(StartingGreeksCsvIndex + 3);
protected set => throw new InvalidOperationException("Theta is read-only.");
}

/// <inheritdoc />
public override decimal Rho
{
get => _csvLine.GetDecimalFromCsv(_startingIndex + 4);
get => _csvLine.GetDecimalFromCsv(StartingGreeksCsvIndex + 4);
protected set => throw new InvalidOperationException("Rho is read-only.");
}

Expand All @@ -373,12 +368,11 @@ public override decimal Lambda
}

/// <summary>
/// Initializes a new default instance of the <see cref="PrecalculatedGreeks"/> class
/// Initializes a new default instance of the <see cref="PreCalculatedGreeks"/> class
/// </summary>
public PrecalculatedGreeks(string csvLine, int startingIndex)
public PreCalculatedGreeks(string csvLine)
{
_csvLine = csvLine;
_startingIndex = startingIndex;
}
}
}
Expand Down
59 changes: 42 additions & 17 deletions Common/Extensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1574,15 +1574,17 @@ public static List<string> ToCsvData(this string str, int size = 4, char delimit
/// <summary>
/// Gets the value at the specified index from a CSV line.
/// </summary>
/// <param name="csvLine">The csv line</param>
/// <param name="index">The value index</param>
/// <returns>The csv value at the given index. Null if the index is out of range.</returns>
/// <param name="csvLine">The CSV line</param>
/// <param name="index">The index of the value to be extracted from the CSV line</param>
/// <param name="result">The value at the given index</param>
/// <returns>Whether there was a value at the given index and could be extracted</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string GetFromCsv(this string csvLine, int index)
public static bool TryGetFromCsv(this string csvLine, int index, out ReadOnlySpan<char> result)
{
if (csvLine.IsNullOrEmpty())
result = ReadOnlySpan<char>.Empty;
if (csvLine.IsNullOrEmpty() || index < 0)
{
return null;
return false;
}

var span = csvLine.AsSpan();
Expand All @@ -1591,7 +1593,7 @@ public static string GetFromCsv(this string csvLine, int index)
var commaIndex = span.IndexOf(',');
if (commaIndex == -1)
{
return null;
return false;
}
span = span.Slice(commaIndex + 1);
}
Expand All @@ -1602,25 +1604,48 @@ public static string GetFromCsv(this string csvLine, int index)
nextCommaIndex = span.Length;
}

return span.Slice(0, nextCommaIndex).ToString();
result = span.Slice(0, nextCommaIndex);
return true;
}

/// <summary>
/// Gets the decimal value at the specified index from a CSV line.
/// Gets the value at the specified index from a CSV line, converted into a decimal.
/// </summary>
/// <param name="csvLine">The csv line</param>
/// <param name="index">The value index</param>
/// <returns>The csv decimal value at the given index. Null if the index is out of range.</returns>
/// <param name="csvLine">The CSV line</param>
/// <param name="index">The index of the value to be extracted from the CSV line</param>
/// <param name="value">The decimal value at the given index</param>
/// <returns>Whether there was a value at the given index and could be extracted and converted into a decimal</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static decimal GetDecimalFromCsv(this string csvLine, int index)
public static bool TryGetDecimalFromCsv(this string csvLine, int index, out decimal value)
{
var csvValue = csvLine.GetFromCsv(index);
if (csvValue.IsNullOrEmpty())
value = decimal.Zero;
if (!csvLine.TryGetFromCsv(index, out var csvValue))
{
return false;
}

try
{
return decimal.Zero;
value = decimal.Parse(csvValue, NumberStyles.Any, CultureInfo.InvariantCulture);
return true;
}
catch (FormatException)
{
return false;
}
}

return csvValue.ToDecimal();
/// <summary>
/// Gets the value at the specified index from a CSV line, converted into a decimal.
/// </summary>
/// <param name="csvLine">The CSV line</param>
/// <param name="index">The index of the value to be extracted from the CSV line</param>
/// <returns>The decimal value at the given index. If the index is invalid or conversion fails, it will return zero</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static decimal GetDecimalFromCsv(this string csvLine, int index)
{
csvLine.TryGetDecimalFromCsv(index, out var value);
return value;
}

/// <summary>
Expand Down
110 changes: 110 additions & 0 deletions Tests/Common/Data/UniverseSelection/OptionUniverseTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/*
* 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 NUnit.Framework;
using QuantConnect.Data;
using QuantConnect.Data.Market;
using QuantConnect.Data.UniverseSelection;
using QuantConnect.Securities;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;

namespace QuantConnect.Tests.Common.Securities.Options
{
[TestFixture, Parallelizable(ParallelScope.Fixtures)]
public class OptionUniverseTests
{
private static string TestOptionUniverseFile = @"
#symbol_id,symbol_value,open,high,low,close,volume,open_interest,implied_volatility,delta,gamma,vega,theta,rho
SPX 31,SPX,5488.47998046875,5523.64013671875,5451.1201171875,5460.47998046875,7199220000,,,,,,,
SPX Z3DLU7UBV6SU|SPX 31,SPX 260618C05400000,780.3000,853.9000,709.6000,767.7500,0,135,0.1637928,0.6382026,0.0002890,26.5721377,-0.5042690,55.5035521
SPX Z8DSNBS7V966|SPX 31,SPX 261218C05400000,893.1400,907.7100,893.1400,907.5400,37,1039,0.1701839,0.6420671,0.0002447,28.9774913,-0.4608812,67.5259867
SPX ZIC7DCXNPVLA|SPX 31,SPX 271217C05400000,1073.0000,1073.0000,1073.0000,1073.0000,0,889,0.1839256,0.6456981,0.0001858,32.6109403,-0.3963479,88.5870185
SPX ZSAM3E33KI0E|SPX 31,SPX 281215C05400000,1248.0000,1248.0000,1248.0000,1248.0000,0,301,0.1934730,0.6472619,0.0001512,35.1083627,-0.3434647,106.9858230
SPX 102FWY2SPYEJY|SPX 31,SPX 291221C05400000,1467.9000,1467.9000,1467.9000,1467.9000,0,9,0.2046702,0.6460372,0.0001254,36.9157598,-0.2993105,122.2236355
SPX YK9CDJQAQJJI|SPX 31,SPX 240719C05405000,95.4500,95.4500,95.4500,95.4500,1,311,0.1006795,0.6960459,0.0026897,4.4991247,-1.4284818,2.0701880
SPX YL0WW5Z0VO1A|SPX 31,SPX 240816C05405000,161.4000,161.4000,161.4000,161.4000,0,380,0.1088739,0.6472976,0.0017128,7.3449930,-1.1139626,4.5112640
SPX YLZDJFRXK2NI|SPX 31,SPX 240920C05405000,213.7000,213.7000,211.0000,211.0000,0,33,0.1149306,0.6316343,0.0012532,9.7567496,-0.9462173,7.4872272
SPX YMQY220NP75A|SPX 31,SPX 241018C05405000,254.0000,303.3500,218.2500,238.0500,0,0,0.1183992,0.6273390,0.0010556,11.2892617,-0.8673778,9.8420483
SPX YK9CCXDVSOI6|SPX 31,SPX 240719C05410000,143.5900,143.5900,119.7100,119.7100,11,355,0.0995106,0.6842402,0.0027673,4.5750811,-1.4291241,2.0364155
SPX YL0WVJMLXSZY|SPX 31,SPX 240816C05410000,151.2000,151.2000,151.2000,151.2000,0,68,0.1080883,0.6395066,0.0017388,7.4027436,-1.1113164,4.4598077
SPX YLZDITFIM7M6|SPX 31,SPX 240920C05410000,202.5000,202.5000,201.9800,201.9800,0,211,0.1142983,0.6258911,0.0012667,9.8073284,-0.9438102,7.4239078
SPX YMQY1FO8RC3Y|SPX 31,SPX 241018C05410000,256.4800,256.4800,255.9000,255.9000,0,91,0.1180060,0.6223570,0.0010637,11.3388534,-0.8661655,9.7694707
SPX YNIIK1WYWGLQ|SPX 31,SPX 241115C05410000,279.7500,279.7500,279.2300,279.2300,0,65,0.1268034,0.6170056,0.0008881,12.7072390,-0.8357895,11.9829003
SPX YK9CDJRY9W1A|SPX 31,SPX 240719C05415000,123.1800,123.1800,98.0300,98.0300,5,307,0.0985516,0.6716430,0.0028403,4.6505424,-1.4312099,2.0001484
SPX YL0WW60OF0J2|SPX 31,SPX 240816C05415000,146.6900,146.6900,146.6900,146.6900,3,901,0.1073207,0.6315307,0.0017645,7.4585091,-1.1084001,4.4069495
SPX YLZDJFTL3F5A|SPX 31,SPX 240920C05415000,194.1000,196.7000,194.1000,196.7000,0,63,0.1136398,0.6200837,0.0012804,9.8561442,-0.9410592,7.3597879
SPX YMQY222B8JN2|SPX 31,SPX 241018C05415000,246.5000,295.7500,210.7500,230.9500,0,0,0.1172852,0.6175838,0.0010746,11.3844988,-0.8632046,9.7014393
SPX YK9CCXE1R0JY|SPX 31,SPX 240719C05420000,119.7500,119.7500,94.0000,94.0000,31,453,0.0973479,0.6589639,0.0029188,4.7207612,-1.4288180,1.9636645
SPX YL0WVJMRW51Q|SPX 31,SPX 240816C05420000,181.5800,181.5800,154.8300,154.8300,4,110,0.1065704,0.6233721,0.0017897,7.5120648,-1.1051922,4.3527055
".TrimStart();

private List<OptionUniverse> _optionUniverseFile;

[OneTimeSetUp]
public void OneTimeSetUp()
{
var config = new SubscriptionDataConfig(typeof(OptionUniverse),
Symbols.SPX,
Resolution.Daily,
TimeZones.NewYork,
TimeZones.NewYork,
true,
true,
false);
var date = new DateTime(2024, 06, 28);

_optionUniverseFile = new List<OptionUniverse>();
var factory = new OptionUniverse();
using var stream = new MemoryStream(Encoding.UTF8.GetBytes(TestOptionUniverseFile));
using var reader = new StreamReader(stream);
while (!reader.EndOfStream)
{
var data = (OptionUniverse)factory.Reader(config, reader, date, false);
if (data == null) continue;
_optionUniverseFile.Add(data);
}
}

[Test]
public void RoundTripCsvConversion()
{
var stringBuilder = new StringBuilder();
stringBuilder.AppendLine("#" + OptionUniverse.CsvHeader);

foreach (var data in _optionUniverseFile)
{
string csv = null;
if (data.Symbol.SecurityType.IsOption())
{
csv = OptionUniverse.ToCsv(data.Symbol, data.Open, data.High, data.Low, data.Close, data.Volume, data.OpenInterest,
data.ImpliedVolatility, data.Greeks);
}
else
{
csv = OptionUniverse.ToCsv(data.Symbol, data.Open, data.High, data.Low, data.Close, data.Volume, null, null, null);
}

stringBuilder.AppendLine(csv);
}

var csvString = stringBuilder.ToString();
Assert.AreEqual(TestOptionUniverseFile, csvString);
}
}
}
63 changes: 62 additions & 1 deletion Tests/Common/Util/ExtensionsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1231,7 +1231,7 @@ public void PyObjectTryConvertSymbolArray()
{
// Wrap a Symbol Array around a PyObject and convert it back
using PyObject value = new PyList(new[] { Symbols.SPY.ToPython(), Symbols.AAPL.ToPython() });


Symbol[] symbols;
var canConvert = value.TryConvert(out symbols);
Expand Down Expand Up @@ -1970,6 +1970,67 @@ public void GetPythonPropertyOfACustomIndicatorWorks(string stringModule,string
}
}

[Test]
public void TryGetFromCsv_EmptyCsv_ReturnsNull()
{
var csvLine = "";
var index = 0;

Assert.IsFalse(csvLine.TryGetFromCsv(index, out var result));
Assert.IsTrue(result.IsEmpty);
}

[Test]
public void TryGetFromCsv_SingleValue_ReturnsValue()
{
var csvLine = "value";
var index = 0;

Assert.IsTrue(csvLine.TryGetFromCsv(index, out var result));
Assert.AreEqual("value", result.ToString());
}

[TestCase("value1,value2,value3", 0, "value1")]
[TestCase("value1,value2,value3", 1, "value2")]
[TestCase("value1,value2,value3", 2, "value3")]
[TestCase("value1,value2,value3,", 0, "value1")]
[TestCase("value1,value2,value3,", 1, "value2")]
[TestCase("value1,value2,value3,", 2, "value3")]
[TestCase("value1,value2,value3,", 3, "")]
public void TryGetFromCsv_MultipleValues_ReturnsCorrectValue(string csvLine, int index, string expectedValue)
{
Assert.IsTrue(csvLine.TryGetFromCsv(index, out var result));
Assert.AreEqual(expectedValue, result.ToString());
}

[TestCase(-1)]
[TestCase(3)]
public void TryGetFromCsv_InvalidIndex_ReturnsNull(int index)
{
var csvLine = "value1,value2,value3";
Assert.IsFalse(csvLine.TryGetFromCsv(index, out var result));
Assert.IsTrue(result.IsEmpty);
}

[TestCase(0)]
[TestCase(-1)]
[TestCase(3)]
public void TryGetDecimalFromCsv_InvalidTypeOrIndex_ReturnsZero(int index)
{
var csvLine = "value1,value2,value3";
Assert.IsFalse(csvLine.TryGetDecimalFromCsv(index, out var result));
Assert.AreEqual(0, result);
}

[TestCase(0, 2.0)]
[TestCase(1, 1.234)]
public void TryGetDecimalFromCsv_ReturnsDecimalValue(int index, decimal expectedValue)
{
var csvLine = "2,1.234";
Assert.IsTrue(csvLine.TryGetDecimalFromCsv(index, out var result));
Assert.AreEqual(expectedValue, result);
}

private PyObject ConvertToPyObject(object value)
{
using (Py.GIL())
Expand Down

0 comments on commit 3327576

Please sign in to comment.