351 lines
17 KiB
C#
351 lines
17 KiB
C#
/*
|
|
* 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.Collections.Generic;
|
|
using System.Globalization;
|
|
using System.Linq;
|
|
using NUnit.Framework;
|
|
using QuantConnect.Logging;
|
|
using QuantConnect.Securities.Option.StrategyMatcher;
|
|
using static QuantConnect.Securities.Option.StrategyMatcher.OptionPositionCollection;
|
|
using static QuantConnect.Securities.Option.StrategyMatcher.OptionStrategyDefinitions;
|
|
using static QuantConnect.Tests.Common.Securities.Options.StrategyMatcher.Option;
|
|
|
|
namespace QuantConnect.Tests.Common.Securities.Options.StrategyMatcher
|
|
{
|
|
[TestFixture]
|
|
public class OptionStrategyDefinitionTests
|
|
{
|
|
[Test]
|
|
[TestCaseSource(nameof(TestCases))]
|
|
public void Run(TestCase test)
|
|
{
|
|
var result = test.Definition.Match(test.Positions).ToList();
|
|
foreach (var match in result)
|
|
{
|
|
Log.Trace(string.Join(";", match.Legs.Select(leg => String(leg.Position))));
|
|
}
|
|
|
|
test.AssertMatch(result);
|
|
}
|
|
|
|
[Test]
|
|
public void CoveredCall_MatchesMultipleTimes_ForEachUniqueShortCallContract()
|
|
{
|
|
// CoveredCall
|
|
// 0: +1 underlying lot
|
|
// 1: -1 Call
|
|
// so we should match
|
|
// (4U, -4C100)
|
|
// (3U, -3C110)
|
|
// (5U, -5C120)
|
|
// (9U, -9C130)
|
|
// (20U, -20C140)
|
|
// OptionStrategyDefinition.Match produces ALL possible matches
|
|
var positions = Empty.AddRange(Position(Underlying, +20),
|
|
Position(Call[100], -4), Position(Put[105], -4),
|
|
Position(Call[105], +4), Position(Put[110], +4),
|
|
Position(Call[110], -3), Position(Put[115], -3),
|
|
Position(Call[115], +3), Position(Put[120], +3),
|
|
Position(Call[120], -5), Position(Put[125], -5),
|
|
Position(Call[125], +5), Position(Put[130], +5),
|
|
Position(Call[130], -9), Position(Put[135], -9),
|
|
Position(Call[140], -21), Position(Put[145], -21)
|
|
);
|
|
|
|
// force lower strikes to be evaluated first to provide determinism for this test
|
|
var options = OptionStrategyMatcherOptions.ForDefinitions(CoveredCall)
|
|
.WithPositionEnumerator(new FunctionalOptionPositionCollectionEnumerator(
|
|
pos => pos.OrderBy(p => p.IsUnderlying ? 0 : p.Strike)
|
|
));
|
|
|
|
var matches = CoveredCall.Match(options, positions).ToList();
|
|
Assert.AreEqual(5, matches.Count);
|
|
Assert.AreEqual(1, matches.Count(m => m.Multiplier == 4));
|
|
Assert.AreEqual(1, matches.Count(m => m.Multiplier == 3));
|
|
Assert.AreEqual(1, matches.Count(m => m.Multiplier == 5));
|
|
Assert.AreEqual(1, matches.Count(m => m.Multiplier == 9));
|
|
Assert.AreEqual(1, matches.Count(m => m.Multiplier == 20));
|
|
}
|
|
|
|
[Test]
|
|
public void DoubleCountPositionsMatchingSamePositionMultipleTimesInDifferentMatches()
|
|
{
|
|
// this test aims to verify that we can match the same definition multiple times if positions allows
|
|
// 1: -C110 +C105
|
|
// 0: -C110 +C100
|
|
// 2: -C115 +C105
|
|
// 3: -C115 +C100
|
|
var positions = Empty.AddRange(
|
|
Position(Call[100], -1),
|
|
Position(Call[105], -1),
|
|
Position(Call[110]),
|
|
Position(Call[115])
|
|
);
|
|
|
|
var matches = BearCallSpread.Match(positions).ToList();
|
|
Assert.AreEqual(4, matches.Count);
|
|
Assert.AreEqual(1, matches.Count(m => m.Legs[1].Position.Strike == 110 && m.Legs[0].Position.Strike == 105));
|
|
Assert.AreEqual(1, matches.Count(m => m.Legs[1].Position.Strike == 110 && m.Legs[0].Position.Strike == 100));
|
|
Assert.AreEqual(1, matches.Count(m => m.Legs[1].Position.Strike == 115 && m.Legs[0].Position.Strike == 105));
|
|
Assert.AreEqual(1, matches.Count(m => m.Legs[1].Position.Strike == 115 && m.Legs[0].Position.Strike == 100));
|
|
}
|
|
|
|
[Test]
|
|
public void ResultingPositionsAreCorrectNoUnderlying()
|
|
{
|
|
var positions = Empty.AddRange(
|
|
Position(Call[100], 10),
|
|
Position(Call[110], -20),
|
|
Position(Call[120], 10)
|
|
);
|
|
|
|
var matches = ButterflyCall.Match(positions).ToList();
|
|
Assert.AreEqual(1, matches.Count);
|
|
Assert.AreEqual(3, matches[0].Legs.Count);
|
|
Assert.AreEqual(1, matches[0].Legs.Count(m => m.Position.Strike == 100 && m.Position.Quantity == 10));
|
|
Assert.AreEqual(1, matches[0].Legs.Count(m => m.Position.Strike == 110 && m.Position.Quantity == -20));
|
|
Assert.AreEqual(1, matches[0].Legs.Count(m => m.Position.Strike == 120 && m.Position.Quantity == 10));
|
|
|
|
// Now let add some extra option contracts which shouldn't match since they aren't enough
|
|
positions = positions.Add(Position(Call[100], 5));
|
|
positions = positions.Add(Position(Call[110], -5));
|
|
|
|
matches = ButterflyCall.Match(positions).ToList();
|
|
|
|
Assert.AreEqual(1, matches.Count);
|
|
Assert.AreEqual(3, matches[0].Legs.Count);
|
|
|
|
// assert the strategy size respects the matching multiplier
|
|
var strategy = matches[0].CreateStrategy();
|
|
Assert.AreEqual(1, strategy.OptionLegs.Count(m => m.Strike == 100 && m.Quantity == 10));
|
|
Assert.AreEqual(1, strategy.OptionLegs.Count(m => m.Strike == 110 && m.Quantity == -20));
|
|
Assert.AreEqual(1, strategy.OptionLegs.Count(m => m.Strike == 120 && m.Quantity == 10));
|
|
|
|
// assert the remaining positions are the expected ones
|
|
positions = matches[0].RemoveFrom(positions);
|
|
Assert.AreEqual(2, positions.Count);
|
|
Assert.AreEqual(1, positions.Count(m => m.Strike == 100 && m.Quantity == 5));
|
|
Assert.AreEqual(1, positions.Count(m => m.Strike == 110 && m.Quantity == -5));
|
|
}
|
|
|
|
[Test]
|
|
public void ResultingPositionsAreCorrectWithUnderlying()
|
|
{
|
|
var positions = Empty.AddRange(
|
|
Position(Call[100], -5),
|
|
// should match 4 covered calls
|
|
new OptionPosition(Underlying, 4)
|
|
);
|
|
|
|
var matches = CoveredCall.Match(positions).ToList();
|
|
Assert.AreEqual(1, matches.Count);
|
|
// underlying isn't included yet
|
|
Assert.AreEqual(1, matches[0].Legs.Count);
|
|
Assert.AreEqual(1, matches[0].Legs.Count(m => m.Position.Strike == 100 && m.Position.Quantity == -5));
|
|
|
|
var strategy = matches[0].CreateStrategy();
|
|
Assert.AreEqual(1, strategy.OptionLegs.Count);
|
|
Assert.AreEqual(1, strategy.UnderlyingLegs.Count);
|
|
Assert.AreEqual(1, strategy.OptionLegs.Count(m => m.Strike == 100 && m.Quantity == -4));
|
|
Assert.AreEqual(1, strategy.UnderlyingLegs.Count(m => m.Quantity == 4));
|
|
|
|
// assert the remaining positions are the expected ones
|
|
positions = matches[0].RemoveFrom(positions);
|
|
Assert.AreEqual(1, positions.Count);
|
|
Assert.AreEqual(1, positions.Count(m => !m.IsUnderlying && m.Strike == 100 && m.Quantity == -1));
|
|
}
|
|
|
|
private static string String(OptionPosition position)
|
|
{
|
|
var sign = position.Quantity > 0 ? "+" : "";
|
|
|
|
var s = position.Symbol;
|
|
var symbol = s.HasUnderlying
|
|
? $"{s.Underlying.Value}:{s.ID.OptionRight}@{s.ID.StrikePrice}:{s.ID.Date:MM-dd}"
|
|
: s.Value;
|
|
|
|
return $"{sign}{position.Quantity} {symbol}";
|
|
}
|
|
|
|
public static TestCaseData[] TestCases
|
|
{
|
|
get
|
|
{
|
|
return new[]
|
|
{
|
|
TestCase.ExactPosition(CoveredCall, Position(Call[100], -1), new OptionPosition(Underlying, +100) ),
|
|
TestCase.ExactPosition(CoveredPut, Position(Put[100], -1), new OptionPosition(Underlying, -100) ),
|
|
|
|
TestCase.ExactPosition(BearCallSpread, Position(Call[110], +1), Position(Call[100], -1)),
|
|
TestCase.ExactPosition(BearCallSpread, Position(Call[100], -1), Position(Call[110], +1)),
|
|
TestCase.ExactPosition(BearPutSpread, Position( Put[110], +1), Position( Put[100], -1)),
|
|
TestCase.ExactPosition(BearPutSpread, Position( Put[100], -1), Position( Put[110], +1)),
|
|
TestCase.ExactPosition(BullCallSpread, Position(Call[110], -1), Position(Call[100], +1)),
|
|
TestCase.ExactPosition(BullCallSpread, Position(Call[100], +1), Position(Call[110], -1)),
|
|
TestCase.ExactPosition(BullPutSpread, Position( Put[110], -1), Position( Put[100], +1)),
|
|
TestCase.ExactPosition(BullPutSpread, Position( Put[100], +1), Position( Put[110], -1)),
|
|
TestCase.ExactPosition(Straddle, Position(Call[100], +1), Position( Put[100], +1)),
|
|
TestCase.ExactPosition(Straddle, Position( Put[100], +1), Position(Call[100], +1)),
|
|
TestCase.ExactPosition(ButterflyCall, Position(Call[100], +1), Position(Call[105], -2), Position(Call[110], +1)),
|
|
TestCase.ExactPosition(ButterflyCall, Position(Call[105], -2), Position(Call[100], +1), Position(Call[110], +1)),
|
|
TestCase.ExactPosition(ButterflyCall, Position(Call[110], +1), Position(Call[105], -2), Position(Call[100], +1)),
|
|
TestCase.ExactPosition(ShortButterflyCall, Position(Call[110], -1), Position(Call[105], +2), Position(Call[100], -1)),
|
|
TestCase.ExactPosition(ButterflyPut, Position( Put[100], +1), Position( Put[105], -2), Position( Put[110], +1)),
|
|
TestCase.ExactPosition(ButterflyPut, Position( Put[105], -2), Position( Put[100], +1), Position( Put[110], +1)),
|
|
TestCase.ExactPosition(ButterflyPut, Position( Put[110], +1), Position( Put[105], -2), Position( Put[100], +1)),
|
|
TestCase.ExactPosition(ShortButterflyPut, Position( Put[110], -1), Position( Put[105], +2), Position( Put[100], -1)),
|
|
|
|
TestCase.ExactPosition(CallCalendarSpread, Position(Call[100, 1], +1), Position(Call[100, 0], -1)),
|
|
TestCase.ExactPosition(CallCalendarSpread, Position(Call[100, 0], -1), Position(Call[100, 1], +1)),
|
|
TestCase.ExactPosition(PutCalendarSpread, Position( Put[100, 1], +1), Position( Put[100, 0], -1)),
|
|
TestCase.ExactPosition(PutCalendarSpread, Position( Put[100, 0], -1), Position( Put[100, 1], +1)),
|
|
|
|
TestCase.ExactPosition(IronCondor, Position( Put[100, 0], +1), Position(Put[105, 0], -1), Position(Call[110, 0], -1), Position(Call[120, 0], +1))
|
|
|
|
}.Select(x => new TestCaseData(x).SetName(x.Name)).ToArray();
|
|
}
|
|
}
|
|
|
|
// aim for perfect match, extra quantity in position, extra symbol position, no match/missing leg
|
|
public class TestCase
|
|
{
|
|
public string Name { get; }
|
|
public OptionStrategyDefinition Definition { get; }
|
|
public OptionPositionCollection Positions { get; }
|
|
public IReadOnlyList<OptionPosition> Extra { get; }
|
|
public IReadOnlyList<OptionPosition> Missing { get; }
|
|
public IReadOnlyDictionary<string, int> ExpectedMatches { get; }
|
|
public OptionStrategyMatcher CreateMatcher()
|
|
=> new OptionStrategyMatcher(new OptionStrategyMatcherOptions(
|
|
new[] {Definition}, new List<int> {100, 100, 100, 100}
|
|
));
|
|
|
|
private readonly string _methodName;
|
|
|
|
private TestCase(
|
|
string methodName,
|
|
OptionStrategyDefinition definition,
|
|
IReadOnlyList<OptionPosition> positions,
|
|
IReadOnlyList<OptionPosition> extra,
|
|
IReadOnlyList<OptionPosition> missing
|
|
)
|
|
{
|
|
_methodName = methodName;
|
|
|
|
Extra = extra;
|
|
Missing = missing;
|
|
Definition = definition;
|
|
Positions = FromPositions(positions);
|
|
var suffix = positions.Select(p =>
|
|
{
|
|
var quantity = p.Quantity.ToString(CultureInfo.InvariantCulture);
|
|
if (p.Quantity > 0)
|
|
{
|
|
quantity = $"+{quantity}";
|
|
}
|
|
|
|
if (p.IsUnderlying)
|
|
{
|
|
return $"{quantity}{p.Symbol.Value}";
|
|
}
|
|
|
|
return $"{quantity}{p.Right.ToString()[0]}@{p.Strike}";
|
|
});
|
|
Name = $"{definition.Name}:{methodName}({string.Join(", ", suffix)})";
|
|
|
|
//int count;
|
|
//if (NameCounts.TryGetValue(Name, out count))
|
|
//{
|
|
// // test runner doesn't like duplicate names -- ensure uniqueness by counting instances of names
|
|
// count++;
|
|
//}
|
|
|
|
//NameCounts[Name] = count;
|
|
//Name = $"{Name} ({count})";
|
|
ExpectedMatches = new Dictionary<string, int>
|
|
{
|
|
{nameof(ExactPosition), 1},
|
|
{nameof(ExtraQuantity), 1},
|
|
{nameof(ExtraPosition), 1},
|
|
{nameof(MissingPosition), 0}
|
|
};
|
|
}
|
|
|
|
public void AssertMatch(List<OptionStrategyDefinitionMatch> matches)
|
|
{
|
|
switch (_methodName)
|
|
{
|
|
case nameof(ExactPosition):
|
|
Assert.AreEqual(ExpectedMatches[_methodName], matches.Count);
|
|
Assert.AreEqual(Definition, matches[0].Definition);
|
|
break;
|
|
|
|
case nameof(ExtraQuantity):
|
|
Assert.AreEqual(ExpectedMatches[_methodName], matches.Count);
|
|
Assert.AreEqual(Definition, matches[0].Definition);
|
|
break;
|
|
|
|
case nameof(ExtraPosition):
|
|
Assert.AreEqual(ExpectedMatches[_methodName], matches.Count);
|
|
Assert.AreEqual(Definition, matches[0].Definition);
|
|
break;
|
|
|
|
case nameof(MissingPosition):
|
|
Assert.AreEqual(0, matches.Count);
|
|
break;
|
|
|
|
default:
|
|
Assert.Fail("Failed to perform assertion.");
|
|
break;
|
|
}
|
|
}
|
|
|
|
public override string ToString()
|
|
{
|
|
return Name;
|
|
}
|
|
|
|
public static TestCase ExactPosition(OptionStrategyDefinition definition, params OptionPosition[] positions)
|
|
{
|
|
return new TestCase(nameof(ExactPosition), definition, positions, Array.Empty<OptionPosition>(), Array.Empty<OptionPosition>());
|
|
}
|
|
|
|
public static TestCase ExtraQuantity(OptionStrategyDefinition definition, params OptionPosition[] positions)
|
|
{
|
|
// add 1 to the first position
|
|
var extra = positions[0].WithQuantity(1);
|
|
var pos = positions.Select((p, i) => i == 0 ? p + extra : p).ToList();
|
|
return new TestCase(nameof(ExtraQuantity), definition, pos, new[] {extra}, Array.Empty<OptionPosition>());
|
|
}
|
|
|
|
public static TestCase ExtraPosition(OptionStrategyDefinition definition, params OptionPosition[] positions)
|
|
{
|
|
// add a random position w/ the same underlying
|
|
var maxStrike = positions.Where(p => p.Symbol.HasUnderlying).Max(p => p.Strike);
|
|
var extra = new OptionPosition(positions[0].Symbol.WithStrike(maxStrike + 5m), 1);
|
|
var pos = positions.Concat(new[] {extra}).ToList();
|
|
return new TestCase(nameof(ExtraPosition), definition, pos, new[] {extra}, Array.Empty<OptionPosition>());
|
|
}
|
|
|
|
public static TestCase MissingPosition(OptionStrategyDefinition definition, OptionPosition missing, params OptionPosition[] positions)
|
|
{
|
|
return new TestCase(nameof(MissingPosition), definition, positions, Array.Empty<OptionPosition>(), new []{missing});
|
|
}
|
|
}
|
|
}
|
|
}
|