18 using System.Collections.Generic;
20 using System.Runtime.CompilerServices;
38 private List<decimal> _uniqueStrikes;
39 private bool _refreshUniqueStrikes;
40 private DateTime _lastExchangeDate;
41 private readonly decimal _underlyingScaleFactor = 1;
74 : base(allData, underlying.EndTime)
78 _refreshUniqueStrikes =
true;
79 _underlyingScaleFactor = underlyingScaleFactor;
88 public void Refresh(IEnumerable<OptionUniverse> allContractsData,
BaseData underlying, DateTime localTime)
90 base.Refresh(allContractsData, localTime);
93 _refreshUniqueStrikes = _lastExchangeDate != localTime.Date;
94 _lastExchangeDate = localTime.Date;
142 return referenceDate;
158 if (_refreshUniqueStrikes || _uniqueStrikes ==
null)
161 _uniqueStrikes = AllSymbols.Select(x => x.ID.StrikePrice)
163 .OrderBy(strikePrice => strikePrice)
165 _refreshUniqueStrikes =
false;
174 var exactPriceFound =
true;
184 exactPriceFound =
false;
186 if (index == ~_uniqueStrikes.Count)
196 var indexMinPrice = index + minStrike;
197 var indexMaxPrice = index + maxStrike;
198 if (!exactPriceFound)
200 if (minStrike < 0 && maxStrike > 0)
204 else if (minStrike > 0)
211 if (indexMinPrice < 0)
215 else if (indexMinPrice >= _uniqueStrikes.Count)
221 if (indexMaxPrice < 0)
226 if (indexMaxPrice >= _uniqueStrikes.Count)
228 indexMaxPrice = _uniqueStrikes.Count - 1;
231 var minPrice = _uniqueStrikes[indexMinPrice];
232 var maxPrice = _uniqueStrikes[indexMaxPrice];
237 var price = data.ID.StrikePrice;
238 return price >= minPrice && price <= maxPrice;
251 return Contracts(contracts => contracts.Where(x => x.Symbol.ID.OptionRight ==
OptionRight.Call));
260 return Contracts(contracts => contracts.Where(x => x.Symbol.ID.OptionRight ==
OptionRight.Put));
272 return SingleContract(
OptionRight.Call, minDaysTillExpiry, strikeFromAtm);
284 return SingleContract(
OptionRight.Put, minDaysTillExpiry, strikeFromAtm);
290 var contractsForExpiry = GetContractsForExpiry(AllSymbols, minDaysTillExpiry);
291 var contracts = contractsForExpiry.Where(x => x.ID.OptionRight == right).ToList();
292 if (contracts.Count == 0)
298 var strike = GetStrike(contracts, strikeFromAtm);
299 var selected = contracts.Single(x => x.ID.StrikePrice == strike);
301 return SymbolList(
new List<Symbol> { selected });
314 return Spread(
OptionRight.Call, minDaysTillExpiry, higherStrikeFromAtm, lowerStrikeFromAtm);
327 return Spread(
OptionRight.Put, minDaysTillExpiry, higherStrikeFromAtm, lowerStrikeFromAtm);
332 if (!lowerStrikeFromAtm.HasValue)
334 lowerStrikeFromAtm = -higherStrikeFromAtm;
337 if (higherStrikeFromAtm <= lowerStrikeFromAtm)
339 throw new ArgumentException(
"Spread(): strike price arguments must be in descending order, "
340 + $
"{nameof(higherStrikeFromAtm)}, {nameof(lowerStrikeFromAtm)}");
344 var contractsForExpiry = GetContractsForExpiry(AllSymbols, minDaysTillExpiry);
345 var contracts = contractsForExpiry.Where(x => x.ID.OptionRight == right).ToList();
346 if (contracts.Count == 0)
352 var lowerStrike = GetStrike(contracts, (decimal)lowerStrikeFromAtm);
353 var lowerStrikeContract = contracts.Single(x => x.ID.StrikePrice == lowerStrike);
354 var higherStrikeContracts = contracts.Where(x => x.ID.StrikePrice > lowerStrike).ToList();
355 if (higherStrikeContracts.Count == 0)
360 var higherStrike = GetStrike(higherStrikeContracts, higherStrikeFromAtm);
361 var higherStrikeContract = higherStrikeContracts.Single(x => x.ID.StrikePrice == higherStrike);
363 return SymbolList(
new List<Symbol> { lowerStrikeContract, higherStrikeContract });
376 return CalendarSpread(
OptionRight.Call, strikeFromAtm, minNearDaysTillExpiry, minFarDaysTillExpiry);
389 return CalendarSpread(
OptionRight.Put, strikeFromAtm, minNearDaysTillExpiry, minFarDaysTillExpiry);
394 if (minFarDaysTillExpiry <= minNearDaysTillExpiry)
396 throw new ArgumentException(
"CalendarSpread(): expiry arguments must be in ascending order, "
397 + $
"{nameof(minNearDaysTillExpiry)}, {nameof(minFarDaysTillExpiry)}");
400 if (minNearDaysTillExpiry < 0)
402 throw new ArgumentException(
"CalendarSpread(): near expiry argument must be positive.");
406 var strike = GetStrike(AllSymbols, strikeFromAtm);
407 var contracts = AllSymbols.Where(x => x.ID.StrikePrice == strike && x.ID.OptionRight == right).ToList();
410 var nearExpiryContract = GetContractsForExpiry(contracts, minNearDaysTillExpiry).SingleOrDefault();
411 if (nearExpiryContract ==
null)
416 var furtherContracts = contracts.Where(x => x.ID.Date > nearExpiryContract.ID.Date).ToList();
417 var farExpiryContract = GetContractsForExpiry(furtherContracts, minFarDaysTillExpiry).SingleOrDefault();
418 if (farExpiryContract ==
null)
423 return SymbolList(
new List<Symbol> { nearExpiryContract, farExpiryContract });
436 if (callStrikeFromAtm <= 0)
438 throw new ArgumentException($
"Strangle(): {nameof(callStrikeFromAtm)} must be positive");
441 if (putStrikeFromAtm >= 0)
443 throw new ArgumentException($
"Strangle(): {nameof(putStrikeFromAtm)} must be negative");
446 return CallPutSpread(minDaysTillExpiry, callStrikeFromAtm, putStrikeFromAtm,
true);
457 return CallPutSpread(minDaysTillExpiry, 0, 0);
470 if (callStrikeFromAtm <= putStrikeFromAtm)
472 throw new ArgumentException(
"ProtectiveCollar(): strike price arguments must be in descending order, "
473 + $
"{nameof(callStrikeFromAtm)}, {nameof(putStrikeFromAtm)}");
476 var filtered = CallPutSpread(minDaysTillExpiry, callStrikeFromAtm, putStrikeFromAtm);
478 var callStrike = filtered.Single(x => x.ID.OptionRight ==
OptionRight.Call).ID.StrikePrice;
479 var putStrike = filtered.Single(x => x.ID.OptionRight ==
OptionRight.Put).ID.StrikePrice;
480 if (callStrike <= putStrike)
497 return CallPutSpread(minDaysTillExpiry, strikeFromAtm, strikeFromAtm);
500 private OptionFilterUniverse CallPutSpread(
int minDaysTillExpiry, decimal callStrikeFromAtm, decimal putStrikeFromAtm,
bool otm =
false)
503 var contracts = GetContractsForExpiry(AllSymbols, minDaysTillExpiry).ToList();
505 var calls = contracts.Where(x => x.ID.OptionRight ==
OptionRight.Call).ToList();
506 var puts = contracts.Where(x => x.ID.OptionRight ==
OptionRight.Put).ToList();
510 calls = calls.Where(x => x.ID.StrikePrice >
Underlying.
Price).ToList();
514 if (calls.Count == 0 || puts.Count == 0)
520 var callStrike = GetStrike(calls, callStrikeFromAtm);
521 var call = calls.Single(x => x.ID.StrikePrice == callStrike);
522 var putStrike = GetStrike(puts, putStrikeFromAtm);
523 var put = puts.Single(x => x.ID.StrikePrice == putStrike);
526 return SymbolList(
new List<Symbol> { call, put });
538 return Butterfly(
OptionRight.Call, minDaysTillExpiry, strikeSpread);
550 return Butterfly(
OptionRight.Put, minDaysTillExpiry, strikeSpread);
555 if (strikeSpread <= 0)
557 throw new ArgumentException(
"ProtectiveCollar(): strikeSpread arguments must be positive");
561 var contractsForExpiry = GetContractsForExpiry(AllSymbols, minDaysTillExpiry);
562 var contracts = contractsForExpiry.Where(x => x.ID.OptionRight == right).ToList();
563 if (contracts.Count == 0)
569 var atmStrike = GetStrike(contracts, 0m);
570 var lowerStrike = GetStrike(contracts.Where(x => x.ID.StrikePrice <
Underlying.
Price && x.ID.StrikePrice < atmStrike), -strikeSpread);
571 var upperStrike = -1m;
572 if (lowerStrike != decimal.MaxValue)
574 upperStrike = atmStrike * 2 - lowerStrike;
578 var filtered = this.Where(x =>
579 x.ID.Date == contracts[0].ID.Date && x.ID.OptionRight == right &&
580 (x.ID.StrikePrice == atmStrike || x.ID.StrikePrice == lowerStrike || x.ID.StrikePrice == upperStrike));
581 if (filtered.Count() != 3)
597 if (strikeSpread <= 0)
599 throw new ArgumentException(
"IronButterfly(): strikeSpread arguments must be positive");
603 var contracts = GetContractsForExpiry(AllSymbols, minDaysTillExpiry).ToList();
607 if (calls.Count == 0 || puts.Count == 0)
613 var atmStrike = GetStrike(contracts, 0);
614 var otmCallStrike = GetStrike(calls.Where(x => x.ID.StrikePrice > atmStrike), strikeSpread);
615 var otmPutStrike = -1m;
616 if (otmCallStrike != decimal.MaxValue)
618 otmPutStrike = atmStrike * 2 - otmCallStrike;
621 var filtered = this.Where(x =>
622 x.ID.Date == contracts[0].ID.Date && (
623 x.ID.StrikePrice == atmStrike ||
624 (x.ID.OptionRight ==
OptionRight.Call && x.ID.StrikePrice == otmCallStrike) ||
625 (x.ID.OptionRight ==
OptionRight.Put && x.ID.StrikePrice == otmPutStrike)
627 if (filtered.Count() != 4)
645 if (nearStrikeSpread <= 0 || farStrikeSpread <= 0)
647 throw new ArgumentException(
"IronCondor(): strike arguments must be positive, "
648 + $
"{nameof(nearStrikeSpread)}, {nameof(farStrikeSpread)}");
651 if (nearStrikeSpread >= farStrikeSpread)
653 throw new ArgumentException(
"IronCondor(): strike arguments must be in ascending orders, "
654 + $
"{nameof(nearStrikeSpread)}, {nameof(farStrikeSpread)}");
658 var contracts = GetContractsForExpiry(AllSymbols, minDaysTillExpiry).ToList();
662 if (calls.Count == 0 || puts.Count == 0)
668 var nearCallStrike = GetStrike(calls, nearStrikeSpread);
669 var nearPutStrike = GetStrike(puts, -nearStrikeSpread);
670 var farCallStrike = GetStrike(calls.Where(x => x.ID.StrikePrice > nearCallStrike), farStrikeSpread);
671 var farPutStrike = -1m;
672 if (farCallStrike != decimal.MaxValue)
674 farPutStrike = nearPutStrike - farCallStrike + nearCallStrike;
678 var filtered = this.Where(x =>
679 x.ID.Date == contracts[0].ID.Date && (
680 (x.ID.OptionRight ==
OptionRight.Call && x.ID.StrikePrice == nearCallStrike) ||
681 (x.ID.OptionRight ==
OptionRight.Put && x.ID.StrikePrice == nearPutStrike) ||
682 (x.ID.OptionRight ==
OptionRight.Call && x.ID.StrikePrice == farCallStrike) ||
683 (x.ID.OptionRight ==
OptionRight.Put && x.ID.StrikePrice == farPutStrike)
685 if (filtered.Count() != 4)
702 if (strikeSpread <= 0)
704 throw new ArgumentException($
"BoxSpread(): strike arguments must be positive, {nameof(strikeSpread)}");
708 var contracts = GetContractsForExpiry(AllSymbols, minDaysTillExpiry).ToList();
709 if (contracts.Count == 0)
715 var higherStrike = GetStrike(contracts.Where(x => x.ID.StrikePrice >
Underlying.
Price), strikeSpread);
716 var lowerStrike = GetStrike(contracts.Where(x => x.ID.StrikePrice < higherStrike && x.ID.StrikePrice <
Underlying.
Price), -strikeSpread);
719 var filtered = this.Where(x =>
720 (x.ID.StrikePrice == higherStrike || x.ID.StrikePrice == lowerStrike) &&
721 x.ID.Date == contracts[0].ID.Date);
722 if (filtered.Count() != 4)
739 if (minFarDaysTillExpiry <= minNearDaysTillExpiry)
741 throw new ArgumentException(
"JellyRoll(): expiry arguments must be in ascending order, "
742 + $
"{nameof(minNearDaysTillExpiry)}, {nameof(minFarDaysTillExpiry)}");
745 if (minNearDaysTillExpiry < 0)
747 throw new ArgumentException(
"JellyRoll(): near expiry argument must be positive.");
751 var strike = AllSymbols.OrderBy(x => Math.Abs(
Underlying.
Price - x.ID.StrikePrice + strikeFromAtm))
752 .First().ID.StrikePrice;
753 var contracts = AllSymbols.Where(x => x.ID.StrikePrice == strike && x.ID.OptionRight ==
OptionRight.Call).ToList();
756 var nearExpiryContract = GetContractsForExpiry(contracts, minNearDaysTillExpiry).SingleOrDefault();
757 if (nearExpiryContract ==
null)
761 var nearExpiry = nearExpiryContract.ID.Date;
763 var furtherContracts = contracts.Where(x => x.ID.Date > nearExpiryContract.ID.Date).ToList();
764 var farExpiryContract = GetContractsForExpiry(furtherContracts, minFarDaysTillExpiry).SingleOrDefault();
765 if (farExpiryContract ==
null)
769 var farExpiry = farExpiryContract.ID.Date;
771 var filtered = this.Where(x => x.ID.StrikePrice == strike && (x.ID.Date == nearExpiry || x.ID.Date == farExpiry));
772 if (filtered.Count() != 4)
790 return Ladder(
OptionRight.Call, minDaysTillExpiry, higherStrikeFromAtm, middleStrikeFromAtm, lowerStrikeFromAtm);
804 return Ladder(
OptionRight.Put, minDaysTillExpiry, higherStrikeFromAtm, middleStrikeFromAtm, lowerStrikeFromAtm);
815 ValidateSecurityTypeForSupportedFilters(nameof(
Delta));
816 return this.Where(contractData => contractData.Greeks.Delta >= min && contractData.Greeks.Delta <= max);
828 return Delta(min, max);
839 ValidateSecurityTypeForSupportedFilters(nameof(
Gamma));
840 return this.Where(contractData => contractData.Greeks.Gamma >= min && contractData.Greeks.Gamma <= max);
852 return Gamma(min, max);
863 ValidateSecurityTypeForSupportedFilters(nameof(
Theta));
864 return this.Where(contractData => contractData.Greeks.Theta >= min && contractData.Greeks.Theta <= max);
876 return Theta(min, max);
887 ValidateSecurityTypeForSupportedFilters(nameof(
Vega));
888 return this.Where(contractData => contractData.Greeks.Vega >= min && contractData.Greeks.Vega <= max);
900 return Vega(min, max);
911 ValidateSecurityTypeForSupportedFilters(nameof(
Rho));
912 return this.Where(contractData => contractData.Greeks.Rho >= min && contractData.Greeks.Rho <= max);
924 return Rho(min, max);
936 return this.Where(contractData => contractData.ImpliedVolatility >= min && contractData.ImpliedVolatility <= max);
959 ValidateSecurityTypeForSupportedFilters(nameof(
OpenInterest));
960 return this.Where(contractData => contractData.OpenInterest >= min && contractData.OpenInterest <= max);
979 #pragma warning disable CA1002 // Do not expose generic lists
980 #pragma warning disable CA2225 // Operator overloads have named alternates
983 return universe.AllSymbols.ToList();
985 #pragma warning restore CA2225 // Operator overloads have named alternates
986 #pragma warning restore CA1002 // Do not expose generic lists
988 private OptionFilterUniverse Ladder(
OptionRight right,
int minDaysTillExpiry, decimal higherStrikeFromAtm, decimal middleStrikeFromAtm, decimal lowerStrikeFromAtm)
990 if (higherStrikeFromAtm <= lowerStrikeFromAtm || higherStrikeFromAtm <= middleStrikeFromAtm || middleStrikeFromAtm <= lowerStrikeFromAtm)
992 throw new ArgumentException(
"Ladder(): strike price arguments must be in descending order, "
993 + $
"{nameof(higherStrikeFromAtm)}, {nameof(middleStrikeFromAtm)}, {nameof(lowerStrikeFromAtm)}");
997 var contracts = GetContractsForExpiry(AllSymbols.Where(x => x.ID.OptionRight == right).ToList(), minDaysTillExpiry);
1000 var lowerStrikeContract = contracts.OrderBy(x => Math.Abs(
Underlying.
Price - x.ID.StrikePrice + lowerStrikeFromAtm)).First();
1001 var middleStrikeContract = contracts.Where(x => x.ID.StrikePrice > lowerStrikeContract.ID.StrikePrice)
1002 .OrderBy(x => Math.Abs(
Underlying.
Price - x.ID.StrikePrice + middleStrikeFromAtm)).FirstOrDefault();
1003 if (middleStrikeContract ==
default)
1007 var higherStrikeContract = contracts.Where(x => x.ID.StrikePrice > middleStrikeContract.ID.StrikePrice)
1008 .OrderBy(x => Math.Abs(
Underlying.
Price - x.ID.StrikePrice + higherStrikeFromAtm)).FirstOrDefault();
1009 if (higherStrikeContract ==
default)
1014 return this.WhereContains(
new List<Symbol> { lowerStrikeContract, middleStrikeContract, higherStrikeContract });
1023 private IEnumerable<Symbol> GetContractsForExpiry(IEnumerable<Symbol> symbols,
int minDaysTillExpiry)
1025 var leastExpiryAccepted = _lastExchangeDate.AddDays(minDaysTillExpiry);
1026 return symbols.Where(x => x.ID.Date >= leastExpiryAccepted)
1027 .GroupBy(x => x.ID.Date)
1028 .OrderBy(x => x.Key)
1031 ?.OrderBy(x => x.ID) ?? Enumerable.Empty<Symbol>();
1048 AllSymbols = contracts;
1052 private decimal GetStrike(IEnumerable<Symbol> symbols, decimal strikeFromAtm)
1054 return symbols.OrderBy(x => Math.Abs(
Underlying.
Price + strikeFromAtm - x.ID.StrikePrice))
1055 .Select(x => x.ID.StrikePrice)
1056 .DefaultIfEmpty(decimal.MaxValue)
1060 [MethodImpl(MethodImplOptions.AggressiveInlining)]
1061 private void ValidateSecurityTypeForSupportedFilters(
string filterName)
1065 throw new InvalidOperationException($
"{filterName} filter is not supported for future options.");
1083 universe.Data = universe.Data.Where(predicate).ToList();
1095 universe.Data = universe.Data.Where(predicate.ConvertToDelegate<Func<OptionUniverse, bool>>()).ToList();
1107 universe.AllSymbols = universe.Data.Select(mapFunc).ToList();
1119 return universe.Select(mapFunc.ConvertToDelegate<Func<OptionUniverse, Symbol>>());
1130 universe.AllSymbols = universe.Data.SelectMany(mapFunc).ToList();
1142 return universe.SelectMany(mapFunc.ConvertToDelegate<Func<
OptionUniverse, IEnumerable<Symbol>>>());
1153 universe.Data = universe.Data.Where(x => filterList.Contains(x)).ToList();
1165 return universe.WhereContains(filterList.ConvertToSymbolEnumerable().ToList());