Overall Statistics |
Total Trades 21 Average Win 2.76% Average Loss -1.74% Compounding Annual Return 32.888% Drawdown 15.200% Expectancy 0.293 Net Profit 30.731% Sharpe Ratio 1.423 Probabilistic Sharpe Ratio 62.713% Loss Rate 50% Win Rate 50% Profit-Loss Ratio 1.59 Alpha 0.06 Beta 0.87 Annual Standard Deviation 0.165 Annual Variance 0.027 Information Ratio 0.252 Tracking Error 0.136 Treynor Ratio 0.269 Total Fees $21.00 Estimated Strategy Capacity $59000000.00 Lowest Capacity Asset NVDA RHM8UTD8DT2D |
namespace QuantConnect { public class Connection { private readonly QCAlgorithm algorithm; // sample API call for MSFT on 2021-01-05 as a reference: //"https://data.nasdaq.com/api/v3/datasets/VOL/MSFT/data?start_date=2021-01-05&end_date=2021-01-05&api_key=9xMzJxNYmHaXix2xTpKt" // base url for API calls private readonly string url = "https://data.nasdaq.com/api/v3/datasets/VOL/"; // API to use in call private readonly string apiKey; // data point entry to look for in call to NASDAQ API (example: IvMean90) private readonly string entry; // column of the data point we are looking for in the NASDAQ API call private int entryIndex = -1; // hash of the current dates stored so they don't have to be refreshed private string hash = "null"; // list of the dates stored under the current hash private List<DateTime> dates = new List<DateTime>(); // dictionary of all symbols, containing another dictoriary of date/price equivalents private Dictionary<String, Dictionary<String, Decimal>> symbolData; // dictionary of all date ranges and whether they are valid for api calls private Dictionary<String, Boolean> dateMap; // boolean which is used to toggle invalid date catcher private bool detectInvalidDates = true; // Connection constructor to import the QCAlgorithm, API key, and entry name public Connection(QCAlgorithm algorithm, string apiKey, string entry) { this.algorithm = algorithm; this.apiKey = apiKey; this.entry = entry; symbolData = new Dictionary<String, Dictionary<String, Decimal>>(); dateMap = new Dictionary<String, Boolean>(); // load all dates into the dateMap from cache if(algorithm.ObjectStore.ContainsKey("DateRangeValidity")) { string data = algorithm.ObjectStore.Read("DateRangeValidity"); string[] ranges = data.Split(','); foreach(string range in ranges) if(range != "") dateMap.Add(range, true); } } // used to retrieve a list of all historical IV values for the prior year // calls the object cache should the value already exist for that month public List<Decimal> Load(string symbol, DateTime date) { Print($"Requesting load for: [symbol={symbol}] [date={date.ToString("yyyy'-'MM'-'dd")}]"); // cross reference hash with current time // if different then load new dates if(hash != $"{date.Year}{date.Month}{date.Day}") { hash = $"{date.Year}{date.Month}{date.Day}"; // get all historical dates dates = new List<DateTime>(); DateTime start = new DateTime(date.Year - 1, date.Month, date.Day); DateTime end = date; for(var dt = start; dt <= end; dt = dt.AddDays(1)) dates.Add(dt); //TODO USE TRADING CALENDAR HERE } return Load(symbol, dates); } private List<Decimal> Load(string symbol, List<DateTime> dates) { // if dates is empty then don't run if(dates.Count() == 0) return new List<Decimal>(); // define storage key (used in the ObjectStore) string key = GetStorageKey(symbol, entry); // if no dates exist within the dictionary, add default if(!symbolData.ContainsKey(symbol)) { symbolData[symbol] = new Dictionary<String, Decimal>(); // if symbol does not exist in cache, do nothing if(!algorithm.ObjectStore.ContainsKey(GetStorageKey(symbol, entry))) { Print($"Cannot find key in object store, loading all dates from API: [key={key}]"); } // found key in cache, load into map else { Print($"Found key in cache, loading all dates found: [key={key}]"); string cached = algorithm.ObjectStore.Read(key); string[] cacheArray = cached.Split(','); //TODO change delimiter to | rather than , between entries for(int i = 0; i < cacheArray.Count(); i += 2) { // out of bounds catch if(i + 1 >= cacheArray.Count()) break; // push data point into map decimal value = -1.0m; if(!Decimal.TryParse(cacheArray[i + 1], out value)) continue; symbolData[symbol][cacheArray[i]] = value; //algorithm.Log($"Loaded from cache: [symbol={symbol}] [date=[{cacheArray[i]}]] [value={cacheArray[i + 1]}]"); } Print($"Loaded all dates into map for: [key={key}]"); } } // create output list List<Decimal> values = new List<Decimal>(); // set the start date to the first date in the list DateTime start = dates[0]; // loop through every date for(int i = 0; i < dates.Count(); i++) { // generate a storage key for the date DateTime current = dates[i]; string date = current.ToString("yyyy'-'MM'-'dd"); // create a boolean to determine if the object exists in the cache bool exists = symbolData[symbol].ContainsKey(date); // if object does not exist in cache move to next date if(exists) { decimal value = symbolData[symbol][date]; // if value is default (-1.0m) then ignore it if(value != -1.0m) values.Add(value); if(start != current) { // if all dates within the range are weekend then ignore the range bool isWeekend = true; for(var dt = start; dt < current; dt = dt.AddDays(1)) if(dt.DayOfWeek != DayOfWeek.Saturday && dt.DayOfWeek != DayOfWeek.Sunday) { isWeekend = false; break; } if(!isWeekend) { //TODO if using trading calendar then preserve this block { // submit request for duration between start and dt Print($"Requesting range from NASDAQ API: [symbol={symbol}] [start_date={start.ToString("yyyy'-'MM'-'dd")}] " + $"[end_date={current.ToString("yyyy'-'MM'-'dd")}]"); values.AddRange(Request(symbol, start, current)); // } } } // update start based on location in dates array // if at end then set "start" to final element in the array if(i != dates.Count() - 1) start = dates[i + 1]; else start = dates[dates.Count() - 1]; } } // do final reuqest validation if the start date doesn't equal the final date in the list if(start != dates[dates.Count() - 1]) { Print($"Requesting range from NASDAQ API: [symbol={symbol}] [start_date={start.ToString("yyyy'-'MM'-'dd")}] " + $"[end_date={dates[dates.Count() - 1].ToString("yyyy'-'MM'-'dd")}]"); values.AddRange(Request(symbol, start, dates[dates.Count() - 1])); } // returns list of all historical IV's retrieved return values; } private List<Decimal> Request(string symbol, DateTime start, DateTime end) { List<Decimal> values = new List<Decimal>(); // get date range string range = GetFormattedRange(start, end); // if invalid range then return empty list if(dateMap.ContainsKey(range) && detectInvalidDates) return values; // retrieve data from NASDAQ API string data = algorithm.Download(GetFormattedURL(symbol, start.Year, start.Month, start.Day, end.Year, end.Month, end.Day)); string[] split = data.Split('\n'); //Print($"{range}: {dateMap.ContainsKey(range)} {split.Count()} {GetFormattedURL(symbol, start.Year, start.Month, start.Day, end.Year, end.Month, end.Day)}"); // if no lines then return empty list // register range as non-valid and add to cache for future runs // conditional is 3 due to formatting reasons from API call including an extra line when requesting weekend if(split.Count() <= 3) { if(detectInvalidDates) dateMap.Add(range, true); cache("DateRangeValidity", range + ","); Print($"Cached {range} to invalid date range"); return values; } // if the entry index has not been parsed yet, find the index of the value if(entryIndex == -1) { // parse columns string[] columns = split[0].Split(','); // look for column with matching name for(int i = 0; i < columns.Count(); i++) { if(columns[i] == entry) { entryIndex = i; break; } } } // output string to append to cache string append = ""; string key = GetStorageKey(symbol, entry); // loop through all data points and find every value for(int i = 1; i < split.Count() - 1; i++) { string[] line = split[i].Split(','); // if missing date then signal error if(line.Count() < 1) { algorithm.Error($"Data issue, malformed line: [symbol={symbol}] [count={values.Count()}]"); return values; } // set date to first data point string date = line[0]; // if less entries then entry index then we know there is an error if(line.Count() <= entryIndex) { algorithm.Error($"Data issue, missing data entry: [symbol={symbol}] [date={date}] [count={values.Count()}]"); return values; } // retrieve value for date and add it to the cache extension and the dictionary decimal value = -1.0m; if(!Decimal.TryParse(line[entryIndex], out value)) continue; symbolData[symbol][date] = value; //Print($"Retrieved value from NASDAQ: [symbol={symbol}] [date={date}] [value={value}]"); values.Add(value); // add value to the cache // get the existing value and append it to the end //TODO change second comma delim to be | append += $"{date},{value},"; //Print($"Cached retrieved value for entry: [key={key}] [date={date}] [value={value}]"); } cache(key, append); Print($"Successfully cached all values for: [dates={GetFormattedRange(start, end)}] [key={key}]"); return values; } // returns formatted url for call to NASDAQ API private string GetFormattedURL(string symbol, int startYear, int startMonth, int startDay, int endYear, int endMonth, int endDay) { string formatted = url; formatted += $"{symbol}/data.csv?order=asc&" + $"start_date={startYear}-{startMonth}-{startDay}" + $"&end_date={endYear}-{endMonth}-{endDay}&api_key={apiKey}"; return formatted; } // returns storage key for ObjectStore hash private string GetStorageKey(string symbol, string entry) { return $"{symbol}-{entry}"; } // returns range based on two dates for determining if valid date range private string GetFormattedRange(DateTime start, DateTime end) { return $"{start.ToString("yyyy'-'MM'-'dd")}:{end.ToString("yyyy'-'MM'-'dd")}"; } // adds a cache to the end of the value stored in the ObjectStore under the given key private void cache(string key, string append) { string existing = ""; if(algorithm.ObjectStore.ContainsKey(key)) existing = algorithm.ObjectStore.Read(key); existing += append; algorithm.ObjectStore.Save(key, append); } private void Print(Object obj) { Console.Write(obj); } } }
namespace QuantConnect.Algorithm.CSharp { public class i_kamanu3 : QCAlgorithm { // BACKTESTING PARAMETERS // ================================================================================================================= // general settings: // set starting cash private int starting_cash = 100000; // backtesting start date time: // date setting variables private int start_year = 2021; private int start_month = 1; private int start_day = 1; // backtesting end date time: // determines whether there is a specified end date // if false it will go to the current date (if 'true' it will go to the specified date) private bool enable_end_date = false; // date setting variables private int end_year = 2021; private int end_month = 1; private int end_day = 1; // universe settings: // number of symbols you want to be observed by the universe at any given time // updates based on the universe resolution set // recommended universe resolution is daily private int stockCount = 10; // data update resolution // changes how often the data updates and algorithm looks for entry // determines how often the function OnData runs // list of resolutions: // Resolution.Tick; Resolution.Second; Resolution.Minute; Resolution.Hour; Resolution.Daily private readonly Resolution resolution = Resolution.Hour; // portfolio allocation // percent of portfolio to allocate to overall (evenly divided between securities) private readonly decimal portfolioAllocation = 0.5m; // API settings: // NASDAQ API key: private readonly string apiKey = "9xMzJxNYmHaXix2xTpKt"; // Data entry from call: // see documentation for details: https://data.nasdaq.com/data/VOL-us-equity-historical-option-implied-volatilities/documentation private readonly string apiDataEntry = "IvMean90"; // algorithm parameters: // high IV select (selects stocks in the top n percentile of high ivs) // note this value is in decimal form: 0.5 = 50% private readonly decimal highIVPercentile = 0.5m; // drawdown percentile (selects stocks with current iv in bottom n percentile of their yearly max iv) // percent of current IV based on the 12 month historical high IV of a security // note this value is in decimal form: 0.5 = 50% private readonly decimal drawdownIVPercentile = 0.25m; // ================================================================================================================= // creates new universe variable setting private List<StockData> universe = new List<StockData>(); // security changes variable private SecurityChanges securityChanges = SecurityChanges.None; // connection object private Connection connection; // month of the current universe private int currentMonth = 0; // determines if universe changed private bool load = false; // iv requirement based on percentile (for log purposes only) private decimal ivRequirement = 0.0m; public override void Initialize() { // function to clear ObjectStore cache //ClearCache(); // set start date SetStartDate(start_year, start_month, start_day); // set end date if(enable_end_date) SetEndDate(end_year, end_month, end_day); // set starting cash SetCash(starting_cash); // add coarse selection for universe AddUniverse(CoarseFilterFunction, FineFilterFunction); // schedule data load for Monday morning before market open Schedule.On(DateRules.Every(DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Friday), TimeRules.At(09, 30), LoadSymbols); // define connection connection = new Connection(this, apiKey, apiDataEntry); } // clears cache: public void ClearCache() { // clear cache: foreach(var key in ObjectStore) { ObjectStore.Delete(key.Key); Log("Successfully deleted: " + key.Key); } } // filter based on CoarseFundamental public IEnumerable<Symbol> CoarseFilterFunction(IEnumerable<CoarseFundamental> coarse) { // check if it is the first of the month, otherwise return current universe if(Time.Month == currentMonth) { return Universe.Unchanged; } currentMonth = Time.Month; load = true; // returns the highest DollarVolume stocks // returns "totalNumberOfStocks" amount of stocks return (from stock in coarse orderby stock.DollarVolume descending select stock.Symbol); } // filters out all symbols not contained in the NASDAQ exchange // takes the top n public IEnumerable<Symbol> FineFilterFunction(IEnumerable<FineFundamental> fine) { return (from stock in fine where stock.SecurityReference.ExchangeId == "NAS" select stock.Symbol).Take(stockCount); } // OnData event is the primary entry point for your algorithm. Each new data point will be pumped in here. // Slice object keyed by symbol containing the stock data private List<StockData> buffer = new List<StockData>(); public override void OnData(Slice data) { // if no securities in buffer then return if(buffer.Count() == 0) return; // set holdings for all securities in buffer foreach(StockData sd in buffer) if(!Securities[sd.ticker].Invested) SetHoldings(sd.ticker, portfolioAllocation / buffer.Count(), false, $"[currentiv={sd.current}] [percentile={sd.ivPercentile}] [ivReq={ivRequirement}]"); // clear buffer buffer.Clear(); } // Loads all Historical IV data into the symbols for the month public void LoadSymbols() { if(!load) return; // list to store all current iv values List<Decimal> currentIVs = new List<Decimal>(); // load each symbol with historical IV data (note last value is current IV) foreach(StockData sd in universe) { // note that parsed is a symbol without the QC tag sd.iv = connection.Load(sd.parsed, Time); //TODO detect if list is too small using the historical iv list (sd.iv) // if no elements then submit error if(sd.iv.Count() == 0) { Error($"No elements recorded for: {sd.parsed}"); continue; } // get and remove last element sd.current = sd.iv[sd.iv.Count() - 1]; sd.iv.Remove(sd.iv.Count() - 1); // push to list for storage currentIVs.Add(sd.current); // calculate percentile of current iv: sd.ivPercentile = CalculatePercentile(sd.iv, sd.current); //Debug($"Drawdown IV percentile calculated: [symbol={sd.parsed}] [currentiv={sd.current}] [value={sd.ivPercentile}]"); } // filter out all stocks that don't meet the requirements: // 1) current iv is in top nth percentile // 2) current iv is in the bottom nth percentile from its 12 month high var highIVSecurities = (from sd in universe where CalculatePercentile(currentIVs, sd.current) >= highIVPercentile where sd.ivPercentile <= drawdownIVPercentile select sd); // push all securities to the buffer foreach(StockData sd in highIVSecurities) { //Debug($"Added symbol to buffer for position entry: [symbol={sd.parsed}] [currentiv={sd.current}] [percentile={sd.ivPercentile}]"); buffer.Add(sd); } // update universe to loaded status load = false; } // used to calculate the percentile of a decimal based on the number of elements // below its value within the array // returns the calculation of: // b = elements below // t = total elements in list // percentile = b / t public decimal CalculatePercentile(IEnumerable<Decimal> list, decimal current) { // if list length is <= 1 then we know it is not properly formatted if(list.Count() <= 1) return Decimal.MaxValue; decimal below = 0.0m; foreach(decimal d in list) if(d < current) below++; decimal percentile = below / list.Count(); return percentile; } // OnSecuritiesChanged runs when the universe updates current securities public override void OnSecuritiesChanged(SecurityChanges changes) { securityChanges = changes; // remove stocks from list that get removed from universe foreach (var security in securityChanges.RemovedSecurities) { List<StockData> stockDatas = universe.Where(x=>x.ticker == security.Symbol).ToList(); if (stockDatas.Count >= 1) { // check to see if position is open and if so close position if(Portfolio[stockDatas.First().ticker].Invested) { // closes position Liquidate(stockDatas.First().ticker); Log($"Liquidated {stockDatas.First().parsed} on removal."); } Log($"Removed {stockDatas.First().parsed} from universe."); // removes stock from list if it is removed from the universe universe.Remove(stockDatas.First()); } } // add new securities to universe list foreach(var security in securityChanges.AddedSecurities) { // create StockData variable for security StockData sd = new StockData(); sd.ticker = security.Symbol; // removes QC code (is symbol value) sd.parsed = sd.ticker.Split(' ')[0]; sd.iv = new List<Decimal>(); sd.ivPercentile = Decimal.MaxValue; // add stockdata to universe universe.Add(sd); Log($"Added {sd.parsed} to universe."); } } // default class containing all ticker information public class StockData { // stock ticker public string ticker = ""; public string parsed = ""; // historical IV values public List<Decimal> iv; // current iv value public decimal current; // iv percentile variables public decimal ivPercentile; } } }