| Overall Statistics |
|
Total Trades 18 Average Win 31.04% Average Loss -21.55% Compounding Annual Return 19.283% Drawdown 11.300% Expectancy 0.220 Net Profit 7.600% Sharpe Ratio 1.226 Probabilistic Sharpe Ratio 55.576% Loss Rate 50% Win Rate 50% Profit-Loss Ratio 1.44 Alpha 0.13 Beta -0.037 Annual Standard Deviation 0.112 Annual Variance 0.012 Information Ratio 1.422 Tracking Error 0.236 Treynor Ratio -3.743 Total Fees $180.00 Estimated Strategy Capacity $3000.00 Lowest Capacity Asset SPY 31Y46M3K287DY|SPY R735QTJ8XC9X Portfolio Turnover 3.37% |
#region imports
using System;
using System.Collections.Generic;
using System.Linq;
using QuantConnect.Brokerages;
using QuantConnect.Util;
using QuantConnect.Indicators;
using QuantConnect.Data;
using QuantConnect.Data.Market;
using QuantConnect.Data.UniverseSelection;
using QuantConnect.Orders;
using QuantConnect.Securities;
using QuantConnect.Securities.Option;
using QuantConnect.Orders.Fees;
#endregion
// sell to open a credit spread during the market’s close for a net credit,
// then buy to close the spread the next day for a net debit.
// Expiry should be 3 or 5 DTE
// We place trades based on the price action of daily candles. Check the daily candle at 3:20 p.m. to see the momentum. The candle’s body should be full, with no large wicks. To be considered a momentum candle, the body must be at least 3/4 full. Please avoid this strategy if the score is 2/4. If a doji or spinning top candle forms, avoid.
// Ichimoku cloud to filter out counter-trend trades.
// Open a 0.14 delta spread. For a bear call spread, we open above the candle’s opening price, and for a bull put spread, we open below it. There is less probability that it will break the opening price if it is a momentum candle.
// Exit — Try to capture 50% premium atleast.
// At market open, the volatility will be high due to power hours. If it’s in our favor. Exit it. Else wait atleast 10:30 to 11 for volatility to decrease.
// If you want to hold longer and get more juice, try exiting with a TTM squeeze fade out. But, as usual, I prefer to be conservative. So my trades are very mechanical, and I like to close with a 50% premium captured.
namespace QuantConnect.Algorithm.CSharp.Options
{
public partial class SellingCreditSpreads : QCAlgorithm
{
string[] tickers = new string[] { "SPY" };
// TODO not used, remove?
decimal maxPercentagePerContract = 1m;
Resolution resolution = Resolution.Minute;
int minExpirationDays = 1;
int maxExpirationDays = 3;
int minStrike = -20;
int maxStrike = 1;
decimal minPrice = 0m;
decimal maxPrice = 0m;
Dictionary<Symbol, Stock> stocks = new Dictionary<Symbol, Stock>();
public override void Initialize()
{
SetCash(2500);
SetStartDate(2022, 1, 1);
SetEndDate(2022, 6, 1);
//SetBrokerageModel(BrokerageName.InteractiveBrokersBrokerage);
SetBrokerageModel(BrokerageName.QuantConnectBrokerage);
AddStocks(this.tickers);
UniverseSettings.DataNormalizationMode = DataNormalizationMode.Raw;
SetSecurityInitializer(x => x.SetDataNormalizationMode(DataNormalizationMode.Raw));
}
void CancelOpenOrders()
{
var openOrders = this.Transactions.GetOpenOrders();
foreach (var order in openOrders)
{
this.Transactions.CancelOrder(order.Id);
}
}
void RemoveAllConsolidators(Symbol symbol)
{
foreach (var subscription in this.SubscriptionManager.Subscriptions.Where(x => x.Symbol == symbol))
{
foreach (var consolidator in subscription.Consolidators)
{
this.SubscriptionManager.RemoveConsolidator(symbol, consolidator);
}
subscription.Consolidators.Clear();
}
}
// Subscribe to stocks
void AddStocks(string[] tickers)
{
foreach (string ticker in tickers)
{
Stock stock = new Stock(ticker, this.resolution, this, this.maxPercentagePerContract, this.minExpirationDays, this.maxExpirationDays, this.minStrike, this.maxStrike, this.minPrice, this.maxPrice);
this.stocks.TryAdd(stock.stockSymbol, stock);
}
}
public override void OnData(Slice slice)
{
if (Time > new DateTime(Time.Year, Time.Month, Time.Day, 15, 20, 0) && Time < new DateTime(Time.Year, Time.Month, Time.Day, 15, 59, 0))
{
this.TryEnter(slice);
}
if (Time >= new DateTime(Time.Year, Time.Month, Time.Day, 9, 30, 0))
{
this.TryExit(slice);
}
}
private void TryEnter(Slice slice)
{
// CancelOpenOrders();
foreach (KeyValuePair<Symbol, Stock> entry in this.stocks)
{
Stock stock = entry.Value;
stock.TryEnter(slice);
}
}
private void TryExit(Slice slice)
{
// CancelOpenOrders();
foreach (KeyValuePair<Symbol, Stock> entry in this.stocks)
{
Stock stock = entry.Value;
stock.TryExit(slice);
}
}
}
class Stock
{
decimal minPrice;
decimal maxPrice;
public Symbol stockSymbol;
public Symbol optionSymbol;
SellingCreditSpreads algorithm;
Indicators indicators;
decimal percentagePerStock;
decimal maxPercentagePerContract;
int minExpirationDays;
int maxExpirationDays;
int minStrike;
int maxStrike;
bool investedPuts = false;
bool investedCalls = false;
public int contracts;
public DateTime expirationDate;
public DateTime createdDate;
public OptionContract shortLeg;
public OptionContract longLeg;
decimal distanceToInsurance = 0.01m;
// percent of strike price
public readonly decimal minDistanceBetweenPutsCalls = 0.02M;
// percent of strike price
public readonly decimal maxDistanceBetweenPutsCalls = 0.07M;
// percent of strike price
private readonly decimal safeDistanceToMaxBet = 0.001M;
public bool DecideIfClosing(Slice slice)
{
bool closing = false;
if(this.CalculatePnlPercentagePerShare(slice) >= 0.5m)
// Use closeInDays
// if ((this.algorithm.Time - this.createdDate).TotalDays >= this.closeInDays)
{
closing = true;
this.Log("Capturing 50% pnl.");
}
// close before expiration
if ((this.expirationDate.Date - this.algorithm.Time.Date).TotalDays <= 0)
{
// just before market close
if (this.algorithm.Time.TimeOfDay >= new TimeSpan(15, 0, 0))
{
closing = true;
}
}
return closing;
}
public TradeBar GetTodayPrices()
{
IEnumerable<TradeBar> history = this.algorithm.History("SPY", 1, Resolution.Daily);
return history.ElementAt(0);
}
public bool DecideIfOpening(Slice slice)
{
bool opening = false;
TradeBar today = GetTodayPrices();
// opening = today.Close > today.Open && today.High <= today.Close && today.Low >= today.Open;
opening = ((today.Close - today.Open)/(today.High - today.Low) > 0.7m)
&& ((today.High - today.Close)/(today.Close - today.Open) < 0.2m)
&& ((today.Open - today.Low)/(today.Close - today.Open) < 0.2m);
// do not open if yesturday was not a business day
// if (!this.IsDayBeforeBusinessDay())
// {
// this.Log("Not entering: yesturday was not a business day.");
// return false;
// }
return opening;
}
public void Log(string message)
{
this.algorithm.Debug(message);
}
public decimal CalculateCurrentPremiumPerShare(Slice slice)
{
decimal shortLegLastPrice = this.algorithm.Securities[this.shortLeg.Symbol].AskPrice;
decimal longLegLastPrice = this.algorithm.Securities[this.longLeg.Symbol].BidPrice;
return shortLegLastPrice - longLegLastPrice;
}
public decimal CalculateInitialPremiumPerShare()
{
return this.averageFillPriceShort - this.averageFillPriceLong;
}
public decimal CalculatePnlPercentagePerShare(Slice slice)
{
decimal initialPremium = CalculateInitialPremiumPerShare();
decimal currentPremium = CalculateCurrentPremiumPerShare(slice);
decimal result = 0;
if(initialPremium != 0)
{
result = (initialPremium - currentPremium)/initialPremium;
}
return result;
}
public decimal CalculateMaxLoss()
{
return Math.Abs(this.shortLeg.Strike - this.longLeg.Strike);
}
public void SetInvestedPuts(bool investedPuts)
{
this.investedPuts = investedPuts;
}
public void SetInvestedCalls(bool investedCalls)
{
this.investedCalls = investedCalls;
}
bool IsDayBeforeExpirationBusinessDay(DateTime expirationDate)
{
return this.algorithm.TradingCalendar.GetTradingDay(expirationDate.AddDays(-1)).BusinessDay;
}
bool IsDayBeforeBusinessDay()
{
return this.algorithm.TradingCalendar.GetTradingDay(this.algorithm.Time.AddDays(-1)).BusinessDay;
}
bool AreAllDaysBeforeExpirationBusinessDays(DateTime expirationDate)
{
int numberOfBusinessDaysUntilExpiration = this.algorithm.TradingCalendar.GetDaysByType(TradingDayType.BusinessDay, this.algorithm.Time, expirationDate).Count();
int numberOfDaysUntilExpiration = (expirationDate - this.algorithm.Time).Days;
// Log("numberOfBusinessDaysUntilExpiration=" + numberOfBusinessDaysUntilExpiration + ", numberOfDaysUntilExpiration" + numberOfDaysUntilExpiration);
return numberOfBusinessDaysUntilExpiration == numberOfDaysUntilExpiration + 2;
}
bool IsNextDayBusinessDay()
{
return this.algorithm.TradingCalendar.GetTradingDay(this.algorithm.Time.AddDays(1)).BusinessDay;
}
public Stock(string underlyingTicker, Resolution resolution, SellingCreditSpreads algorithm, decimal maxPercentagePerContract, int minExpirationDays, int maxExpirationDays, int minStrike, int maxStrike, decimal minPrice, decimal maxPrice)
{
this.algorithm = algorithm;
this.createdDate = algorithm.Time;
this.stockSymbol = this.algorithm.AddEquity(underlyingTicker, resolution).Symbol;
var option = this.algorithm.AddOption(underlyingTicker, Resolution.Minute);
option.PriceModel = OptionPriceModels.CrankNicolsonFD();
option.SetFilter(universe => from symbol in universe.Strikes(this.minStrike, this.maxStrike).WeeklysOnly().Expiration(TimeSpan.FromDays(this.minExpirationDays), TimeSpan.FromDays(this.maxExpirationDays)) select symbol);
this.optionSymbol = option.Symbol;
this.indicators = new Indicators(algorithm, this.stockSymbol, resolution);
this.maxPercentagePerContract = maxPercentagePerContract;
this.algorithm.Securities[this.stockSymbol].SetDataNormalizationMode(DataNormalizationMode.Raw);
this.minExpirationDays = minExpirationDays;
this.maxExpirationDays = maxExpirationDays;
this.minStrike = minStrike;
this.maxStrike = maxStrike;
this.minPrice = minPrice;
this.maxPrice = maxPrice;
}
bool IsTradable(Symbol symbol)
{
Security security;
if (this.algorithm.Securities.TryGetValue(symbol, out security))
{
return security.IsTradable;
}
return false;
}
bool IsTradable()
{
return IsTradable(this.optionSymbol);
}
int CalculateNumberOfContracts()
{
var percentagePerContract = this.maxPercentagePerContract;
int numberOfContracts = (int)((this.algorithm.Portfolio.MarginRemaining) / (100m * (this.shortLeg.Strike - this.longLeg.Strike)));
return numberOfContracts;
}
public OptionContract FindContractWithDelta(Slice slice, OptionRight right)
{
TradeBar today = GetTodayPrices();
OptionContract foundContract = null;
OptionChain chain;
if (slice.OptionChains.TryGetValue(this.optionSymbol, out chain))
{
if (right == OptionRight.Put)
{
foundContract = (
from contract in chain
.OrderByDescending(contract => (Math.Abs(contract.Greeks.Delta)))
where Math.Abs(contract.Greeks.Delta) <= 0.14m
where contract.Right == right
where contract.Strike < contract.UnderlyingLastPrice
where contract.Strike < today.Open
select contract
).FirstOrDefault();
}
else
{
foundContract = (
from contract in chain
.OrderByDescending(contract => (contract.OpenInterest * contract.LastPrice))
where contract.Right == right
where contract.Strike > contract.UnderlyingLastPrice
select contract
).FirstOrDefault();
}
}
return foundContract;
}
// find put contract with max dollar bet: Open Interest * LastPrice, with strike below/above underlying price
public OptionContract FindContractWithMaxDollarBet(Slice slice, OptionRight right)
{
OptionContract foundContract = null;
OptionChain chain;
if (slice.OptionChains.TryGetValue(this.optionSymbol, out chain))
{
if (right == OptionRight.Put)
{
foundContract = (
from contract in chain
.OrderByDescending(contract => (contract.OpenInterest * contract.LastPrice))
where contract.Right == right
where contract.Strike < contract.UnderlyingLastPrice
select contract
).FirstOrDefault();
}
else
{
foundContract = (
from contract in chain
.OrderByDescending(contract => (contract.OpenInterest * contract.LastPrice))
where contract.Right == right
where contract.Strike > contract.UnderlyingLastPrice
select contract
).FirstOrDefault();
}
}
return foundContract;
}
/// <summary>
/// Finds contract that has a distance from mainContract.
/// Examples: insurance contract, less risky contract.
/// </summary>
/// <param name="slice"></param>Option chains to find contract in.
/// <param name="mainContract"></param>Main contract distance from which is specified.
/// <param name="distance"></param>Distance from main contract, percent of strike price.
/// <returns>Found contract or null.</returns>
private OptionContract FindContractWithDistance(Slice slice, OptionContract mainContract, decimal distance)
{
OptionContract foundContract = null;
OptionChain chain;
if (slice.OptionChains.TryGetValue(this.optionSymbol, out chain))
{
if (mainContract.Right == OptionRight.Put)
{
// first contract below strike of main contract
foundContract = (
from contract in chain
.OrderByDescending(contract => contract.Strike)
where contract.Right == mainContract.Right
where contract.Expiry == mainContract.Expiry
where contract.Strike < mainContract.Strike
select contract
).FirstOrDefault();
}
else
{
// first contract above strike of main contract
foundContract = (
from contract in chain
.OrderByDescending(contract => contract.Strike)
where contract.Right == mainContract.Right
where contract.Expiry == mainContract.Expiry
where contract.Strike > mainContract.Strike
select contract
).LastOrDefault();
}
}
return foundContract;
}
public OptionRight GetOppositeRight(OptionRight right)
{
if (right == OptionRight.Put)
{
return OptionRight.Call;
}
else
{
return OptionRight.Put;
}
}
public decimal CalculateDistanceBetweenPutsCalls(Decimal strike1, Decimal strike2)
{
decimal distanceBetweenPutsCalls = Math.Abs(strike1 - strike2);
// Log("distanceBetweenPutsCalls=[" + distanceBetweenPutsCalls + "]");
return distanceBetweenPutsCalls;
}
public OptionContract FindContractWithMaxDollarBetAndGoodDistanceFromOppositeContract(Slice slice, OptionRight right)
{
OptionContract maxBet = this.FindContractWithMaxDollarBet(slice, right);
Log("Contract WithMaxDollarBet=[" + maxBet.Strike + "]");
OptionContract oppositeBet = this.FindContractWithMaxDollarBet(slice, GetOppositeRight(maxBet.Right));
decimal distanceBetweenPutsCalls = CalculateDistanceBetweenPutsCalls(oppositeBet.Strike, maxBet.Strike);
if (distanceBetweenPutsCalls < maxBet.UnderlyingLastPrice * this.minDistanceBetweenPutsCalls)
{
Log("distanceBetweenPutsCalls is too small=[" + distanceBetweenPutsCalls + "]");
return null;
}
else if (distanceBetweenPutsCalls > maxBet.UnderlyingLastPrice * this.maxDistanceBetweenPutsCalls)
{
Log("distanceBetweenPutsCalls is too big=[" + distanceBetweenPutsCalls + "]");
return null;
}
// do not use max bet, use safer bet than safeDistanceToMaxBet less risky
return this.FindContractWithDistance(slice, maxBet, this.safeDistanceToMaxBet);
}
public void TryEnter(Slice slice)
{
if (!this.IsInvested())
{
if (this.algorithm.Securities[this.stockSymbol].Exchange.ExchangeOpen)
{
// this.indicators.CalculateFromHistory();
if (DecideIfOpening(slice))
{
this.shortLeg = this.FindContractWithDelta(slice, OptionRight.Put);
if (this.shortLeg != null)
{
// do not open if not all days before expiration are business days
if (!this.IsNextDayBusinessDay())
{
this.Log("Not entering: tomorrow is not business day.");
return;
}
// find insurance contract
this.longLeg = this.FindContractWithDistance(slice, this.shortLeg, this.distanceToInsurance);
if (this.longLeg != null)
{
// TODO
if (IsTradable(this.shortLeg.Symbol))
{
// this.contracts = CalculateNumberOfContracts();
this.contracts = 20;
if (this.contracts > 0)
{
Open();
this.expirationDate = shortLeg.Expiry;
}
else
{
Log("Not enough cash");
}
}
}
}
}
}
}
}
public void TryExit(Slice slice)
{
if (this.IsInvested())
{
if (DecideIfClosing(slice))
{
if (this.algorithm.Securities[this.stockSymbol].Exchange.ExchangeOpen)
{
Close();
}
}
}
}
decimal averageFillPriceLong;
decimal averageFillPriceShort;
public void Open()
{
string tag = "Opening";
{
decimal feePerOrder = 0.5m * this.contracts;
this.algorithm.Securities[this.shortLeg.Symbol].FeeModel = new ConstantFeeModel(feePerOrder);
this.algorithm.Securities[this.longLeg.Symbol].FeeModel = new ConstantFeeModel(feePerOrder);
var optionStrategy = OptionStrategies.BullPutSpread(this.optionSymbol, this.shortLeg.Strike, this.longLeg.Strike, this.longLeg.Expiry);
var tickets = this.algorithm.Buy(optionStrategy, this.contracts, true, tag);
foreach (var ticket in tickets)
{
if (ticket.Status != OrderStatus.Filled)
{
throw new Exception("Failed to fill order=[" + ticket.Symbol + "], [" + ticket.Quantity + "]");
}
if(ticket.Quantity > 0)
{
this.averageFillPriceLong = ticket.AverageFillPrice;
}
else
{
this.averageFillPriceShort = ticket.AverageFillPrice;
}
}
}
Log("================================= Opened spread=[" + shortLeg.Strike + ", " + longLeg.Strike + "]");
this.SetInvestedCalls(true);
this.SetInvestedPuts(true);
}
public void Close()
{
string tag = "Closing";
{
decimal feePerOrder = 0.5m * this.contracts;
this.algorithm.Securities[this.shortLeg.Symbol].FeeModel = new ConstantFeeModel(feePerOrder);
this.algorithm.Securities[this.longLeg.Symbol].FeeModel = new ConstantFeeModel(feePerOrder);
var optionStrategy = OptionStrategies.BullPutSpread(this.optionSymbol, this.shortLeg.Strike, this.longLeg.Strike, this.longLeg.Expiry);
var tickets = this.algorithm.Sell(optionStrategy, this.contracts, true, tag);
foreach (var ticket in tickets)
{
if (ticket.Status != OrderStatus.Filled)
{
throw new Exception("Failed to fill order=[" + ticket.Symbol + "], [" + ticket.Quantity + "]");
}
}
}
this.SetInvestedCalls(false);
this.SetInvestedPuts(false);
Log("================================= Closed spread=[" + shortLeg.Strike + ", " + longLeg.Strike + "]");
}
public bool ShouldExit()
{
return true;
}
public bool ShouldSellPut()
{
return this.indicators.ShouldSellPut();
}
public bool ShouldSellCall()
{
return this.indicators.ShouldSellCall();
}
private bool IsInvested()
{
return this.investedPuts; // TODO && this.investedCalls;
}
public bool ShouldBuy()
{
return this.percentagePerStock <= 0 && this.indicators.ShouldBuy() && this.IsTradable();
}
public bool ShouldSell()
{
return this.percentagePerStock >= 0 && this.indicators.ShouldSell() && this.IsTradable();
}
public bool ShouldShort()
{
return this.percentagePerStock >= 0 && this.indicators.ShouldShort() && this.IsTradable();
}
public bool ShouldCover()
{
return this.percentagePerStock <= 0 && this.indicators.ShouldCover() && this.IsTradable();
}
public bool AreIndicatorsReady()
{
return this.indicators.IsReady();
}
public bool UpdateIndicators(TradeBar tradeBar)
{
return this.indicators.Update(tradeBar);
}
public void ResetIndicators()
{
this.indicators.Reset();
}
}
public class Indicators
{
QCAlgorithm algorithm;
Symbol symbol;
decimal price;
MovingAverageConvergenceDivergence macd;
ExponentialMovingAverage ema20;
ExponentialMovingAverage ema50;
AroonOscillator aroon;
// Keep history for 15 days
RollingWindow<AroonOscillatorState> aroonHistory = new RollingWindow<AroonOscillatorState>(15);
Resolution resolution;
public Indicators(QCAlgorithm algorithm, Symbol symbol, Resolution resolution)
{
this.algorithm = algorithm;
this.symbol = symbol;
this.resolution = resolution;
this.macd = new MovingAverageConvergenceDivergence(12, 26, 9, MovingAverageType.Wilders);
this.ema20 = new ExponentialMovingAverage((int)(3 * 6.5 * 60));
this.ema50 = new ExponentialMovingAverage(50); ;
this.aroon = new AroonOscillator(20, 20);
}
public void Reset()
{
this.macd.Reset();
this.ema20.Reset();
this.ema50.Reset();
this.aroon.Reset();
}
public bool ShouldBuy()
{
if (IsReady())
{
if (IsMacdInUptrend() && IsEma20AboveEma50())
{
if (this.price > this.ema20)
{
if (HasAroonCrossedUp() && IsAroonAbove50())
{
return true;
}
}
}
}
return false;
}
public bool ShouldCover()
{
if (IsReady())
{
if (IsMacdInUptrend() || IsEma20AboveEma50())
{
if (this.price > this.ema20)
{
if (HasAroonCrossedUp() && IsAroonAbove50())
{
return true;
}
}
}
}
return false;
}
public bool ShouldSell()
{
if (IsReady())
{
if (!IsMacdInUptrend() || !IsEma20AboveEma50())
{
if (this.price < this.ema20)
{
if (HasAroonCrossedDown() && IsAroonAbove50())
{
return true;
}
}
}
}
return false;
}
public bool ShouldShort()
{
if (IsReady())
{
if (!IsMacdInUptrend() && !IsEma20AboveEma50())
{
// if (this.price < this.ema20)
{
if (HasAroonCrossedDown() && IsAroonAbove50())
{
return true;
}
}
}
}
return false;
}
public void CalculateFromHistory()
{
this.Reset();
IEnumerable<TradeBar> history = this.algorithm.History(this.symbol, 100, this.resolution);
foreach (TradeBar tradeBar in history)
{
this.Update(tradeBar);
}
}
public bool Update(TradeBar tradeBar)
{
DateTime time = tradeBar.EndTime;
this.price = tradeBar.Close;
bool result = this.macd.Update(time, price) && this.ema20.Update(time, price) && this.ema50.Update(time, price) && this.aroon.Update(tradeBar);
SaveAroonHistory();
return result;
}
public bool IsReady()
{
return (this.macd.IsReady && this.ema20.IsReady && this.ema50.IsReady && this.aroon.IsReady && this.aroonHistory.IsReady);
}
public bool IsMacdInUptrend()
{
return (this.macd > this.macd.Signal);
}
public bool IsEma20AboveEma50()
{
return (this.ema20 > this.ema50);
}
public bool HasAroonCrossedUp()
{
// cross up between 2 days ago and today
return this.aroon.AroonUp.Current.Value > this.aroon.AroonDown.Current.Value && this.aroonHistory[2].Up < this.aroonHistory[2].Down;
}
public bool HasAroonCrossedDown()
{
// cross down between 2 days ago and today
return this.aroon.AroonUp.Current.Value < this.aroon.AroonDown.Current.Value && this.aroonHistory[2].Up > this.aroonHistory[2].Down;
}
public bool IsAroonAbove50()
{
return this.aroon.AroonUp.Current.Value > 55 && this.aroon.AroonDown.Current.Value > 53;
}
public void SaveAroonHistory()
{
this.aroonHistory.Add(new AroonOscillatorState(this.aroon));
}
internal bool ShouldSellPut()
{
return this.IsReady() && IsMacdInUptrend();
}
internal bool ShouldSellCall()
{
return this.IsReady() && !this.IsEma20AboveEma50();
}
}
// class to hold the current state of a AroonOscillator instance
public class AroonOscillatorState
{
public readonly decimal Up;
public readonly decimal Down;
public AroonOscillatorState(AroonOscillator aroon)
{
Up = aroon.AroonUp.Current.Value;
Down = aroon.AroonDown.Current.Value;
}
}
}