/// <summary> /// Calculate Stochastic Oscillator, as described here: /// <see href="https://en.wikipedia.org/wiki/Stochastic_oscillator"/> /// </summary> /// <param name="series">input time series</param> /// <param name="n">oscillator period</param> /// <param name="parentId">caller cache id, optional</param> /// <param name="memberName">caller's member name, optional</param> /// <param name="lineNumber">caller line number, optional</param> /// <returns>Stochastic Oscillator as time series</returns> public static _StochasticOscillator StochasticOscillator(this ITimeSeries <double> series, int n = 14, CacheId parentId = null, [CallerMemberName] string memberName = "", [CallerLineNumber] int lineNumber = 0) { var cacheId = new CacheId(parentId, memberName, lineNumber, series.GetHashCode(), n); var container = Cache <_StochasticOscillator> .GetData( cacheId, () => new _StochasticOscillator()); double hh = series .Highest(n, cacheId)[0]; double ll = series .Lowest(n, cacheId)[0]; double price = series[0]; container.PercentK = IndicatorsBasic.BufferedLambda( v => 100.0 * (price - ll) / Math.Max(1e-10, hh - ll), 50.0, cacheId); container.PercentD = container.PercentK .SMA(3, cacheId); return(container); }
/// <summary> /// Calculate range over the specified number of past bars. /// </summary> /// <param name="series">input time series</param> /// <param name="n">number of bars to search</param> /// <returns>range between highest and lowest value of past n bars</returns> public static ITimeSeries <double> Range(this ITimeSeries <double> series, int n) { return(series .Highest(n) .Subtract(series .Lowest(n))); }
/// <summary> /// Calculate range over the specified number of past bars. /// </summary> /// <param name="series">input time series</param> /// <param name="n">number of bars to search</param> /// <param name="parentId">caller cache id, optional</param> /// <param name="memberName">caller's member name, optional</param> /// <param name="lineNumber">caller line number, optional</param> /// <returns>range between highest and lowest value of past n bars</returns> public static ITimeSeries <double> Range(this ITimeSeries <double> series, int n, CacheId parentId = null, [CallerMemberName] string memberName = "", [CallerLineNumber] int lineNumber = 0) { var cacheId = new CacheId(parentId, memberName, lineNumber, series.GetHashCode(), n); return(series .Highest(n, cacheId) .Subtract(series .Lowest(n, cacheId), cacheId)); }
/// <summary> /// Return current drawdown in percent, as value between 0 and 1. /// </summary> /// <param name="series">input time series</param> /// <param name="n">length of observation window</param> /// <param name="parentId">caller cache id, optional</param> /// <param name="memberName">caller's member name, optional</param> /// <param name="lineNumber">caller line number, optional</param> /// <returns>drawdown as time series</returns> public static ITimeSeries <double> Drawdown(this ITimeSeries <double> series, int n, CacheId parentId = null, [CallerMemberName] string memberName = "", [CallerLineNumber] int lineNumber = 0) { var cacheId = new CacheId(parentId, memberName, lineNumber, series.GetHashCode(), n); // TODO: rewrite this, using buffered lambda, see MaxDrawdown return(IndicatorsBasic.Const(1.0, cacheId) .Subtract( series .Divide( series .Highest(n, cacheId), cacheId), cacheId)); }
/// <summary> /// Calculate volatility estimate from recent trading range. /// </summary> /// <param name="series">input time series</param> /// <param name="n">length of calculation window</param> /// <param name="parentId">caller cache id, optional</param> /// <param name="memberName">caller's member name, optional</param> /// <param name="lineNumber">caller line number, optional</param> /// <returns>volatility as time series</returns> public static ITimeSeries <double> VolatilityFromRange(this ITimeSeries <double> series, int n, CacheId parentId = null, [CallerMemberName] string memberName = "", [CallerLineNumber] int lineNumber = 0) { var cacheId = new CacheId(parentId, memberName, lineNumber, series.GetHashCode(), n); return(IndicatorsBasic.BufferedLambda( (v) => { double hi = series.Highest(n)[0]; double lo = series.Lowest(n)[0]; return 0.80 * Math.Sqrt(1.0 / n) * Math.Log(hi / lo); }, 0.0, cacheId)); }
/// <summary> /// Calculate Williams %R, as described here: /// <see href="https://en.wikipedia.org/wiki/Williams_%25R"/> /// </summary> /// <param name="series">input time series</param> /// <param name="n">period</param> /// <param name="parentId">caller cache id, optional</param> /// <param name="memberName">caller's member name, optional</param> /// <param name="lineNumber">caller line number, optional</param> /// <returns>Williams %R as time series</returns> public static ITimeSeries <double> WilliamsPercentR(this ITimeSeries <double> series, int n = 10, CacheId parentId = null, [CallerMemberName] string memberName = "", [CallerLineNumber] int lineNumber = 0) { var cacheId = new CacheId(parentId, memberName, lineNumber, series.GetHashCode(), n); return(IndicatorsBasic.BufferedLambda( (v) => { double hh = series.Highest(n)[0]; double ll = series.Lowest(n)[0]; double price = series[0]; return -100.0 * (hh - price) / Math.Max(1e-10, hh - ll); }, -50.0, cacheId)); }
override public void Run() { //---------- initialization // set simulation time frame StartTime = DateTime.Parse("01/01/2017", CultureInfo.InvariantCulture); EndTime = DateTime.Parse("08/01/2018", CultureInfo.InvariantCulture); // set account value Deposit(_initialCash); CommissionPerShare = 0.01; // add instruments // the underlying must be added explicitly, // as the simulation engine requires it AddDataSource(_underlyingNickname); AddDataSource(_optionsNickname); //---------- simulation // loop through all bars foreach (DateTime simTime in SimTimes) { // find the underlying instrument // we could also find the underlying from the option chain _underlyingInstrument = _underlyingInstrument ?? FindInstrument(_underlyingNickname); // retrieve the underlying spot price double underlyingPrice = _underlyingInstrument.Close[0]; _initialUnderlyingPrice = _initialUnderlyingPrice ?? underlyingPrice; // calculate volatility ITimeSeries <double> volatilitySeries = _underlyingInstrument.Close .Volatility(10) .Multiply(Math.Sqrt(252.0)); double volatility = Math.Max( volatilitySeries.EMA(21)[0], volatilitySeries.Highest(5)[0]); // find all expiry dates on the 3rd Friday of the month List <DateTime> expiryDates = OptionChain(_optionsNickname) .Where(o => o.OptionExpiry.DayOfWeek == DayOfWeek.Friday && o.OptionExpiry.Day >= 15 && o.OptionExpiry.Day <= 21 || o.OptionExpiry.DayOfWeek == DayOfWeek.Saturday && o.OptionExpiry.Day >= 16 && o.OptionExpiry.Day <= 22) .Select(o => o.OptionExpiry) .Distinct() .ToList(); // select expiry 3 to 4 weeks out DateTime expiryDate = expiryDates .Where(d => (d - simTime).TotalDays >= 21 && (d - simTime).TotalDays <= 28) .FirstOrDefault(); // retrieve option chain for this expiry List <Instrument> optionChain = OptionChain(_optionsNickname) .Where(o => o.OptionIsPut && o.OptionExpiry == expiryDate) .ToList(); // if we are currently flat, attempt to open a position if (Positions.Count == 0) { // determine strike price: far away from spot price double strikePrice = _underlyingInstrument.Close[0] / Math.Exp(1.75 * Math.Sqrt((expiryDate - simTime).TotalDays / 365.25) * volatility); // find contract closest to our desired strike Instrument shortPut = optionChain .OrderBy(o => Math.Abs(o.OptionStrike - strikePrice)) .FirstOrDefault(); // enter short put position if (shortPut != null) { // Interactive Brokers margin requirements for short naked puts: // Put Price + Maximum((15 % * Underlying Price - Out of the Money Amount), // (10 % * Strike Price)) double margin = Math.Max(0.15 * underlyingPrice - Math.Max(0.0, underlyingPrice - shortPut.OptionStrike), 0.10 * underlyingPrice); int contracts = (int)Math.Floor(Math.Max(0.0, _regTMarginToUse * Cash / (100.0 * margin))); shortPut.Trade(-contracts, OrderType.closeThisBar) .Comment = "open"; } } // monitor and maintain existing positions else // if (Postions.Count != 0) { // find our currently open position // we might need fancier code, in case we have more than // one position open Instrument shortPut = Positions.Keys.First(); // re-evaluate the likely trading range double expectedLowestPrice = _underlyingInstrument.Close[0] / Math.Exp(0.60 * Math.Sqrt((shortPut.OptionExpiry - simTime).Days / 365.25) * volatility); // exit, when the risk of ending in the money is too high // and, the contract is actively traded if (expectedLowestPrice < shortPut.OptionStrike && shortPut.BidVolume[0] > 0 && shortPut.Ask[0] < 2 * shortPut.Bid[0]) { shortPut.Trade(-Positions[shortPut], OrderType.closeThisBar) .Comment = "exit early"; } } // plot the underlying against our strategy results, plus volatility _plotter.SelectChart("nav vs time", "time"); // this will go to Sheet1 _plotter.SetX(simTime); _plotter.Plot(_underlyingInstrument.Symbol, underlyingPrice / (double)_initialUnderlyingPrice); _plotter.Plot("volatility", volatilitySeries[0]); _plotter.Plot("net asset value", NetAssetValue[0] / _initialCash); } //---------- post-processing // create a list of trades on Sheet2 _plotter.SelectChart("trades", "time"); foreach (LogEntry entry in Log) { _plotter.SetX(entry.BarOfExecution.Time); _plotter.Plot("action", entry.Action); _plotter.Plot("instr", entry.Symbol); _plotter.Plot("qty", entry.OrderTicket.Quantity); _plotter.Plot("fill", entry.FillPrice); _plotter.Plot("comment", entry.OrderTicket.Comment ?? ""); } FitnessValue = NetAssetValue[0]; }