| Overall Statistics |
|
Total Trades 12 Average Win 0.40% Average Loss -2.15% Compounding Annual Return 30.619% Drawdown 12.100% Expectancy -0.703 Net Profit 28.438% Sharpe Ratio 1.471 Probabilistic Sharpe Ratio 65.090% Loss Rate 75% Win Rate 25% Profit-Loss Ratio 0.19 Alpha 0.046 Beta 0.856 Annual Standard Deviation 0.147 Annual Variance 0.022 Information Ratio 0.151 Tracking Error 0.116 Treynor Ratio 0.252 Total Fees $12.00 Estimated Strategy Capacity $98000000.00 Lowest Capacity Asset AMD R735QTJ8XC9X |
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
private readonly string entry;
// index of the entry within the call (so we don't scan for it every time)
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
Dictionary<String, Dictionary<String, Decimal>> symbolData;
// 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>>();
}
// 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);
}
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
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(',');
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
foreach(DateTime current in dates) {
// generate a storage key for the date
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) {
// 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));
}
start = current;
}
}
// 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]));
}
return values;
}
private List<Decimal> Request(string symbol, DateTime start, DateTime end) {
List<Decimal> values = new List<Decimal>();
string data = algorithm.Download(GetFormattedURL(symbol, start.Year, start.Month, start.Day, end.Year, end.Month, end.Day));
string[] split = data.Split('\n');
// if no lines then return empty list
if(split.Count() <= 2)
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 cacheExtension = "";
string key = GetStorageKey(symbol, entry);
if(algorithm.ObjectStore.ContainsKey(key))
cacheExtension = algorithm.ObjectStore.Read(key);
// loop through all entry 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 = Convert.ToDecimal(line[entryIndex]);
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
cacheExtension += $"{date},{value},";
//Print($"Cached retrieved value for entry: [key={key}] [date={date}] [value={value}]");
}
algorithm.ObjectStore.Save(key, cacheExtension);
Print($"Successfully cached all values for: [key={key}]");
return values;
}
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;
}
private string GetStorageKey(string symbol, string entry) {
return $"{symbol}-{entry}";
}
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) {
sd.iv = connection.Load(sd.parsed, Time);
// 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}]");
}
// calculate high iv percentile requirement
decimal requiredHighIV = CalculateValue(currentIVs, highIVPercentile);
ivRequirement = requiredHighIV;
// 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 sd.current >= requiredHighIV
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) {
decimal below = 0.0m;
foreach(decimal d in list)
if(d < current)
below++;
decimal percentile = below / list.Count();
return percentile;
}
// used to calculate the value of the nth percentile of a list
// reference: https://stackoverflow.com/questions/8137391/percentile-calculation
public decimal CalculateValue(IEnumerable<Decimal> list, decimal percentile) {
// parse to array
var array = list.ToArray();
// sort the array
Array.Sort(array);
// calculate real index, floored index, and fraction
decimal realIndex = percentile * (array.Count() - 1);
int intIndex = (int)realIndex;
decimal fraction = realIndex - intIndex;
if(intIndex + 1 < array.Count())
return (array[intIndex] * (1 - fraction)) + (array[intIndex + 1] * fraction);
else
return array[intIndex];
}
// 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
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;
}
}
}