18 using System.Collections.Generic;
37 private List<decimal> _uniqueStrikes;
38 private bool _refreshUniqueStrikes;
39 private DateTime _lastExchangeDate;
40 private readonly decimal _underlyingScaleFactor = 1;
73 : base(allData, underlying.EndTime)
77 _refreshUniqueStrikes =
true;
78 _underlyingScaleFactor = underlyingScaleFactor;
87 public void Refresh(IEnumerable<OptionUniverse> allContractsData,
BaseData underlying, DateTime localTime)
89 base.Refresh(allContractsData, localTime);
92 _refreshUniqueStrikes = _lastExchangeDate != localTime.Date;
93 _lastExchangeDate = localTime.Date;
150 return referenceDate;
166 if (_refreshUniqueStrikes || _uniqueStrikes ==
null)
169 _uniqueStrikes = AllSymbols.Select(x => x.ID.StrikePrice)
171 .OrderBy(strikePrice => strikePrice)
173 _refreshUniqueStrikes =
false;
182 var exactPriceFound =
true;
192 exactPriceFound =
false;
194 if (index == ~_uniqueStrikes.Count)
204 var indexMinPrice = index + minStrike;
205 var indexMaxPrice = index + maxStrike;
206 if (!exactPriceFound)
208 if (minStrike < 0 && maxStrike > 0)
212 else if (minStrike > 0)
219 if (indexMinPrice < 0)
223 else if (indexMinPrice >= _uniqueStrikes.Count)
229 if (indexMaxPrice < 0)
234 if (indexMaxPrice >= _uniqueStrikes.Count)
236 indexMaxPrice = _uniqueStrikes.Count - 1;
239 var minPrice = _uniqueStrikes[indexMinPrice];
240 var maxPrice = _uniqueStrikes[indexMaxPrice];
245 var price = data.ID.StrikePrice;
246 return price >= minPrice && price <= maxPrice;
259 return Contracts(contracts => contracts.Where(x => x.Symbol.ID.OptionRight ==
OptionRight.Call));
268 return Contracts(contracts => contracts.Where(x => x.Symbol.ID.OptionRight ==
OptionRight.Put));
280 return SingleContract(
OptionRight.Call, minDaysTillExpiry, strikeFromAtm);
292 return SingleContract(
OptionRight.Put, minDaysTillExpiry, strikeFromAtm);
298 var contractsForExpiry = GetContractsForExpiry(AllSymbols, minDaysTillExpiry);
299 var contracts = contractsForExpiry.Where(x => x.ID.OptionRight == right).ToList();
300 if (contracts.Count == 0)
306 var strike = GetStrike(contracts, strikeFromAtm);
307 var selected = contracts.Single(x => x.ID.StrikePrice == strike);
309 return SymbolList(
new List<Symbol> { selected });
322 return Spread(
OptionRight.Call, minDaysTillExpiry, higherStrikeFromAtm, lowerStrikeFromAtm);
335 return Spread(
OptionRight.Put, minDaysTillExpiry, higherStrikeFromAtm, lowerStrikeFromAtm);
340 if (!lowerStrikeFromAtm.HasValue)
342 lowerStrikeFromAtm = -higherStrikeFromAtm;
345 if (higherStrikeFromAtm <= lowerStrikeFromAtm)
347 throw new ArgumentException(
"Spread(): strike price arguments must be in descending order, "
348 + $
"{nameof(higherStrikeFromAtm)}, {nameof(lowerStrikeFromAtm)}");
352 var contractsForExpiry = GetContractsForExpiry(AllSymbols, minDaysTillExpiry);
353 var contracts = contractsForExpiry.Where(x => x.ID.OptionRight == right).ToList();
354 if (contracts.Count == 0)
360 var lowerStrike = GetStrike(contracts, (decimal)lowerStrikeFromAtm);
361 var lowerStrikeContract = contracts.Single(x => x.ID.StrikePrice == lowerStrike);
362 var higherStrikeContracts = contracts.Where(x => x.ID.StrikePrice > lowerStrike).ToList();
363 if (higherStrikeContracts.Count == 0)
368 var higherStrike = GetStrike(higherStrikeContracts, higherStrikeFromAtm);
369 var higherStrikeContract = higherStrikeContracts.Single(x => x.ID.StrikePrice == higherStrike);
371 return SymbolList(
new List<Symbol> { lowerStrikeContract, higherStrikeContract });
384 return CalendarSpread(
OptionRight.Call, strikeFromAtm, minNearDaysTillExpiry, minFarDaysTillExpiry);
397 return CalendarSpread(
OptionRight.Put, strikeFromAtm, minNearDaysTillExpiry, minFarDaysTillExpiry);
402 if (minFarDaysTillExpiry <= minNearDaysTillExpiry)
404 throw new ArgumentException(
"CalendarSpread(): expiry arguments must be in ascending order, "
405 + $
"{nameof(minNearDaysTillExpiry)}, {nameof(minFarDaysTillExpiry)}");
408 if (minNearDaysTillExpiry < 0)
410 throw new ArgumentException(
"CalendarSpread(): near expiry argument must be positive.");
414 var strike = GetStrike(AllSymbols, strikeFromAtm);
415 var contracts = AllSymbols.Where(x => x.ID.StrikePrice == strike && x.ID.OptionRight == right).ToList();
418 var nearExpiryContract = GetContractsForExpiry(contracts, minNearDaysTillExpiry).SingleOrDefault();
419 if (nearExpiryContract ==
null)
424 var furtherContracts = contracts.Where(x => x.ID.Date > nearExpiryContract.ID.Date).ToList();
425 var farExpiryContract = GetContractsForExpiry(furtherContracts, minFarDaysTillExpiry).SingleOrDefault();
426 if (farExpiryContract ==
null)
431 return SymbolList(
new List<Symbol> { nearExpiryContract, farExpiryContract });
444 if (callStrikeFromAtm <= 0)
446 throw new ArgumentException($
"Strangle(): {nameof(callStrikeFromAtm)} must be positive");
449 if (putStrikeFromAtm >= 0)
451 throw new ArgumentException($
"Strangle(): {nameof(putStrikeFromAtm)} must be negative");
454 return CallPutSpread(minDaysTillExpiry, callStrikeFromAtm, putStrikeFromAtm,
true);
465 return CallPutSpread(minDaysTillExpiry, 0, 0);
478 if (callStrikeFromAtm <= putStrikeFromAtm)
480 throw new ArgumentException(
"ProtectiveCollar(): strike price arguments must be in descending order, "
481 + $
"{nameof(callStrikeFromAtm)}, {nameof(putStrikeFromAtm)}");
484 var filtered = CallPutSpread(minDaysTillExpiry, callStrikeFromAtm, putStrikeFromAtm);
486 var callStrike = filtered.Single(x => x.ID.OptionRight ==
OptionRight.Call).ID.StrikePrice;
487 var putStrike = filtered.Single(x => x.ID.OptionRight ==
OptionRight.Put).ID.StrikePrice;
488 if (callStrike <= putStrike)
505 return CallPutSpread(minDaysTillExpiry, strikeFromAtm, strikeFromAtm);
508 private OptionFilterUniverse CallPutSpread(
int minDaysTillExpiry, decimal callStrikeFromAtm, decimal putStrikeFromAtm,
bool otm =
false)
511 var contracts = GetContractsForExpiry(AllSymbols, minDaysTillExpiry).ToList();
513 var calls = contracts.Where(x => x.ID.OptionRight ==
OptionRight.Call).ToList();
514 var puts = contracts.Where(x => x.ID.OptionRight ==
OptionRight.Put).ToList();
518 calls = calls.Where(x => x.ID.StrikePrice >
Underlying.
Price).ToList();
522 if (calls.Count == 0 || puts.Count == 0)
528 var callStrike = GetStrike(calls, callStrikeFromAtm);
529 var call = calls.Single(x => x.ID.StrikePrice == callStrike);
530 var putStrike = GetStrike(puts, putStrikeFromAtm);
531 var put = puts.Single(x => x.ID.StrikePrice == putStrike);
534 return SymbolList(
new List<Symbol> { call, put });
546 return Butterfly(
OptionRight.Call, minDaysTillExpiry, strikeSpread);
558 return Butterfly(
OptionRight.Put, minDaysTillExpiry, strikeSpread);
563 if (strikeSpread <= 0)
565 throw new ArgumentException(
"ProtectiveCollar(): strikeSpread arguments must be positive");
569 var contractsForExpiry = GetContractsForExpiry(AllSymbols, minDaysTillExpiry);
570 var contracts = contractsForExpiry.Where(x => x.ID.OptionRight == right).ToList();
571 if (contracts.Count == 0)
577 var atmStrike = GetStrike(contracts, 0m);
578 var lowerStrike = GetStrike(contracts.Where(x => x.ID.StrikePrice <
Underlying.
Price && x.ID.StrikePrice < atmStrike), -strikeSpread);
579 var upperStrike = -1m;
580 if (lowerStrike != decimal.MaxValue)
582 upperStrike = atmStrike * 2 - lowerStrike;
586 var filtered = this.Where(x =>
587 x.ID.Date == contracts[0].ID.Date && x.ID.OptionRight == right &&
588 (x.ID.StrikePrice == atmStrike || x.ID.StrikePrice == lowerStrike || x.ID.StrikePrice == upperStrike));
589 if (filtered.Count() != 3)
605 if (strikeSpread <= 0)
607 throw new ArgumentException(
"IronButterfly(): strikeSpread arguments must be positive");
611 var contracts = GetContractsForExpiry(AllSymbols, minDaysTillExpiry).ToList();
615 if (calls.Count == 0 || puts.Count == 0)
621 var atmStrike = GetStrike(contracts, 0);
622 var otmCallStrike = GetStrike(calls.Where(x => x.ID.StrikePrice > atmStrike), strikeSpread);
623 var otmPutStrike = -1m;
624 if (otmCallStrike != decimal.MaxValue)
626 otmPutStrike = atmStrike * 2 - otmCallStrike;
629 var filtered = this.Where(x =>
630 x.ID.Date == contracts[0].ID.Date && (
631 x.ID.StrikePrice == atmStrike ||
632 (x.ID.OptionRight ==
OptionRight.Call && x.ID.StrikePrice == otmCallStrike) ||
633 (x.ID.OptionRight ==
OptionRight.Put && x.ID.StrikePrice == otmPutStrike)
635 if (filtered.Count() != 4)
653 if (nearStrikeSpread <= 0 || farStrikeSpread <= 0)
655 throw new ArgumentException(
"IronCondor(): strike arguments must be positive, "
656 + $
"{nameof(nearStrikeSpread)}, {nameof(farStrikeSpread)}");
659 if (nearStrikeSpread >= farStrikeSpread)
661 throw new ArgumentException(
"IronCondor(): strike arguments must be in ascending orders, "
662 + $
"{nameof(nearStrikeSpread)}, {nameof(farStrikeSpread)}");
666 var contracts = GetContractsForExpiry(AllSymbols, minDaysTillExpiry).ToList();
670 if (calls.Count == 0 || puts.Count == 0)
676 var nearCallStrike = GetStrike(calls, nearStrikeSpread);
677 var nearPutStrike = GetStrike(puts, -nearStrikeSpread);
678 var farCallStrike = GetStrike(calls.Where(x => x.ID.StrikePrice > nearCallStrike), farStrikeSpread);
679 var farPutStrike = -1m;
680 if (farCallStrike != decimal.MaxValue)
682 farPutStrike = nearPutStrike - farCallStrike + nearCallStrike;
686 var filtered = this.Where(x =>
687 x.ID.Date == contracts[0].ID.Date && (
688 (x.ID.OptionRight ==
OptionRight.Call && x.ID.StrikePrice == nearCallStrike) ||
689 (x.ID.OptionRight ==
OptionRight.Put && x.ID.StrikePrice == nearPutStrike) ||
690 (x.ID.OptionRight ==
OptionRight.Call && x.ID.StrikePrice == farCallStrike) ||
691 (x.ID.OptionRight ==
OptionRight.Put && x.ID.StrikePrice == farPutStrike)
693 if (filtered.Count() != 4)
710 if (strikeSpread <= 0)
712 throw new ArgumentException($
"BoxSpread(): strike arguments must be positive, {nameof(strikeSpread)}");
716 var contracts = GetContractsForExpiry(AllSymbols, minDaysTillExpiry).ToList();
717 if (contracts.Count == 0)
723 var higherStrike = GetStrike(contracts.Where(x => x.ID.StrikePrice >
Underlying.
Price), strikeSpread);
724 var lowerStrike = GetStrike(contracts.Where(x => x.ID.StrikePrice < higherStrike && x.ID.StrikePrice <
Underlying.
Price), -strikeSpread);
727 var filtered = this.Where(x =>
728 (x.ID.StrikePrice == higherStrike || x.ID.StrikePrice == lowerStrike) &&
729 x.ID.Date == contracts[0].ID.Date);
730 if (filtered.Count() != 4)
747 if (minFarDaysTillExpiry <= minNearDaysTillExpiry)
749 throw new ArgumentException(
"JellyRoll(): expiry arguments must be in ascending order, "
750 + $
"{nameof(minNearDaysTillExpiry)}, {nameof(minFarDaysTillExpiry)}");
753 if (minNearDaysTillExpiry < 0)
755 throw new ArgumentException(
"JellyRoll(): near expiry argument must be positive.");
759 var strike = AllSymbols.OrderBy(x => Math.Abs(
Underlying.
Price - x.ID.StrikePrice + strikeFromAtm))
760 .First().ID.StrikePrice;
761 var contracts = AllSymbols.Where(x => x.ID.StrikePrice == strike && x.ID.OptionRight ==
OptionRight.Call).ToList();
764 var nearExpiryContract = GetContractsForExpiry(contracts, minNearDaysTillExpiry).SingleOrDefault();
765 if (nearExpiryContract ==
null)
769 var nearExpiry = nearExpiryContract.ID.Date;
771 var furtherContracts = contracts.Where(x => x.ID.Date > nearExpiryContract.ID.Date).ToList();
772 var farExpiryContract = GetContractsForExpiry(furtherContracts, minFarDaysTillExpiry).SingleOrDefault();
773 if (farExpiryContract ==
null)
777 var farExpiry = farExpiryContract.ID.Date;
779 var filtered = this.Where(x => x.ID.StrikePrice == strike && (x.ID.Date == nearExpiry || x.ID.Date == farExpiry));
780 if (filtered.Count() != 4)
798 return Ladder(
OptionRight.Call, minDaysTillExpiry, higherStrikeFromAtm, middleStrikeFromAtm, lowerStrikeFromAtm);
812 return Ladder(
OptionRight.Put, minDaysTillExpiry, higherStrikeFromAtm, middleStrikeFromAtm, lowerStrikeFromAtm);
823 return this.Where(contractData => contractData.Greeks.Delta >= min && contractData.Greeks.Delta <= max);
835 return Delta(min, max);
846 return this.Where(contractData => contractData.Greeks.Gamma >= min && contractData.Greeks.Gamma <= max);
858 return Gamma(min, max);
869 return this.Where(contractData => contractData.Greeks.Theta >= min && contractData.Greeks.Theta <= max);
881 return Theta(min, max);
892 return this.Where(contractData => contractData.Greeks.Vega >= min && contractData.Greeks.Vega <= max);
904 return Vega(min, max);
915 return this.Where(contractData => contractData.Greeks.Rho >= min && contractData.Greeks.Rho <= max);
927 return Rho(min, max);
938 return this.Where(contractData => contractData.ImpliedVolatility >= min && contractData.ImpliedVolatility <= max);
961 return this.Where(contractData => contractData.OpenInterest >= min && contractData.OpenInterest <= max);
980 #pragma warning disable CA1002 // Do not expose generic lists
981 #pragma warning disable CA2225 // Operator overloads have named alternates
984 return universe.AllSymbols.ToList();
986 #pragma warning restore CA2225 // Operator overloads have named alternates
987 #pragma warning restore CA1002 // Do not expose generic lists
989 private OptionFilterUniverse Ladder(
OptionRight right,
int minDaysTillExpiry, decimal higherStrikeFromAtm, decimal middleStrikeFromAtm, decimal lowerStrikeFromAtm)
991 if (higherStrikeFromAtm <= lowerStrikeFromAtm || higherStrikeFromAtm <= middleStrikeFromAtm || middleStrikeFromAtm <= lowerStrikeFromAtm)
993 throw new ArgumentException(
"Ladder(): strike price arguments must be in descending order, "
994 + $
"{nameof(higherStrikeFromAtm)}, {nameof(middleStrikeFromAtm)}, {nameof(lowerStrikeFromAtm)}");
998 var contracts = GetContractsForExpiry(AllSymbols.Where(x => x.ID.OptionRight == right).ToList(), minDaysTillExpiry);
1001 var lowerStrikeContract = contracts.OrderBy(x => Math.Abs(
Underlying.
Price - x.ID.StrikePrice + lowerStrikeFromAtm)).First();
1002 var middleStrikeContract = contracts.Where(x => x.ID.StrikePrice > lowerStrikeContract.ID.StrikePrice)
1003 .OrderBy(x => Math.Abs(
Underlying.
Price - x.ID.StrikePrice + middleStrikeFromAtm)).FirstOrDefault();
1004 if (middleStrikeContract ==
default)
1008 var higherStrikeContract = contracts.Where(x => x.ID.StrikePrice > middleStrikeContract.ID.StrikePrice)
1009 .OrderBy(x => Math.Abs(
Underlying.
Price - x.ID.StrikePrice + higherStrikeFromAtm)).FirstOrDefault();
1010 if (higherStrikeContract ==
default)
1015 return this.WhereContains(
new List<Symbol> { lowerStrikeContract, middleStrikeContract, higherStrikeContract });
1024 private IEnumerable<Symbol> GetContractsForExpiry(IEnumerable<Symbol> symbols,
int minDaysTillExpiry)
1026 var leastExpiryAccepted = _lastExchangeDate.AddDays(minDaysTillExpiry);
1027 return symbols.Where(x => x.ID.Date >= leastExpiryAccepted)
1028 .GroupBy(x => x.ID.Date)
1029 .OrderBy(x => x.Key)
1032 ?.OrderBy(x => x.ID) ?? Enumerable.Empty<Symbol>();
1049 AllSymbols = contracts;
1053 private decimal GetStrike(IEnumerable<Symbol> symbols, decimal strikeFromAtm)
1055 return symbols.OrderBy(x => Math.Abs(
Underlying.
Price + strikeFromAtm - x.ID.StrikePrice))
1056 .Select(x => x.ID.StrikePrice)
1057 .DefaultIfEmpty(decimal.MaxValue)
1075 universe.Data = universe.Data.Where(predicate).ToList();
1087 universe.Data = universe.Data.Where(predicate.ConvertToDelegate<Func<OptionUniverse, bool>>()).ToList();
1099 universe.AllSymbols = universe.Data.Select(mapFunc).ToList();
1111 return universe.Select(mapFunc.ConvertToDelegate<Func<OptionUniverse, Symbol>>());
1122 universe.AllSymbols = universe.Data.SelectMany(mapFunc).ToList();
1134 return universe.SelectMany(mapFunc.ConvertToDelegate<Func<
OptionUniverse, IEnumerable<Symbol>>>());
1145 universe.Data = universe.Data.Where(x => filterList.Contains(x)).ToList();
1157 return universe.WhereContains(filterList.ConvertToSymbolEnumerable().ToList());