| Overall Statistics |
|
Total Trades 7 Average Win 0% Average Loss 0% Compounding Annual Return 16.263% Drawdown 4.500% Expectancy 0 Net Profit 9.374% Sharpe Ratio 1.829 Probabilistic Sharpe Ratio 74.562% Loss Rate 0% Win Rate 0% Profit-Loss Ratio 0 Alpha 0.063 Beta 0.427 Annual Standard Deviation 0.061 Annual Variance 0.004 Information Ratio -0.042 Tracking Error 0.072 Treynor Ratio 0.263 Total Fees $7.00 Estimated Strategy Capacity $160000000.00 Lowest Capacity Asset AMD R735QTJ8XC9X |
/*
This program was developed by Quantify and is a template program.
Usage and marketing of this program is permitted.
www.quantify-co.com
*/
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 = 5;
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 = 15;
// 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 top n% of stocks with the highest IV values)
// note this value is in decimal form: 0.5 = 50%
private readonly decimal highIVSelection = 0.5m;
// drawdown percentile
// 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 securityIVPercentile = 0.5m;
// =================================================================================================================
// 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;
public override void Initialize()
{
// 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);
// 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);
}
// 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).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 than meet conditional
foreach(StockData sd in buffer)
if(sd.percentile < securityIVPercentile && !Securities[sd.ticker].Invested)
SetHoldings(sd.ticker, portfolioAllocation / buffer.Count());
// clear buffer
buffer.Clear();
}
// Loads all Historical IV data into the symbols for the month
public void LoadSymbols() {
if(!load)
return;
// 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);
// find max
decimal max = sd.iv[0];
foreach(decimal d in sd.iv)
if(d > max)
max = d;
// determines if the maximum is faulty
if(max == 0.0m)
sd.percentile = Decimal.MaxValue;
else
sd.percentile = sd.current / max;
// calculate percentile based on maximum iv
//Debug($"Percentile - {sd.parsed} = {sd.percentile} [{sd.current} / {max}]");
}
// selection amount
int n1 = Decimal.ToInt32(Math.Ceiling(highIVSelection * stockCount));
var highIVSecurities = (from sd in universe
orderby sd.current descending
select sd).Take(n1);
// push all securities to the buffer
foreach(StockData sd in highIVSecurities)
buffer.Add(sd);
// update universe to loaded status
load = false;
}
// 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.percentile = 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 percentile;
}
}
}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 key;
// 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>();
// Connection constructor to import the QCAlgorithm, API key, and entry name
public Connection(QCAlgorithm algorithm, string key, string entry) {
this.algorithm = algorithm;
this.key = key;
this.entry = entry;
}
// 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 time) {
// cross reference hash with current time
// if different then load new dates
if(hash != $"{time.Year}{time.Month}{time.Day}") {
hash = $"{time.Year}{time.Month}{time.Day}";
// get all historical dates
dates = new List<DateTime>();
DateTime start = new DateTime(time.Year - 1, time.Month, time.Day);
DateTime end = time;
for(var dt = start; dt <= end; dt = dt.AddMonths(1))
dates.Add(dt);
}
// request each date and add it to a list
List<Decimal> values = new List<Decimal>();
foreach(DateTime dt in dates) {
decimal requested = Request(symbol, dt.Year, dt.Month, dt.Day, time);
// if invalid date
if(requested == -1.0m)
algorithm.Log($"Warning: Unable to retrieve IV for {symbol} on {dt.Year}-{dt.Month}-{dt.Day}.");
else
values.Add(requested);
}
return values;
}
// requests the historical IV value for the given date
// takes the symbol, date, and current date as parameters
private decimal Request(string symbol, int year, int month, int day, DateTime bounds) {
// check for cache existing
string key = Hash(symbol, entry, year, month);
// if it exists then return stored value
if (algorithm.ObjectStore.ContainsKey(key)) {
decimal stored = Convert.ToDecimal(algorithm.ObjectStore.Read(key));
Print($"Read hash for: {key} = {stored}");
return stored;
}
// download and parse data from URL request
Print($"Requesting: {Format(symbol, year, month, day)}");
string data = algorithm.Download(Format(symbol, year, month, day));
// parse by line
string[] split = data.Split('\n');
Console.WriteLine(String.Join(",", split));
// if only 2 lines then request the next day
// if past the DateTime bound then return -1
if(split.Count() <= 2) {
DateTime dt;
// make sure testing valid date
bool valid = DateTime.TryParse($"{month}/{day + 1}/{year}", out dt);
// if invalid date or out of bounds, return -1
if(!valid || dt > bounds) {
Print($"Invalid: {year}-{month}-{day+1} :: {dt > bounds}");
return -1;
}
Print($"Requesting Next Day: {year}-{month}-{day+1}");
// request next day
return Request(symbol, year, month, day + 1, bounds);
}
// 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;
}
}
}
// parse value from data
string[] values = split[1].Split(',');
if(values.Count() <= entryIndex) {
algorithm.Error($"Data issue, [entry points={values.Count()}]");
return -1.0m;
}
//Console.WriteLine(String.Join(",", values));
decimal value = 0.1m;
//decimal value = Convert.ToDecimal(values[entryIndex]);
Print($"Found Value: {year}-{month}-{day} {value}");
// cache found value
//Console.Write($"Created hash: {Hash(symbol, entry, year, month)}");
algorithm.ObjectStore.Save(Hash(symbol, entry, year, month), $"{value}");
return value;
}
private string Format(string symbol, int year, int month, int day) {
string formatted = url;
formatted += $"{symbol}/data.csv?start_date={year}-{month}-{day}" +
$"&end_date={year}-{month}-{day}&api_key={key}";
return formatted;
}
private string Hash(string symbol, string entry, int year, int month) {
return $"{symbol}-{entry}:{year}-{month}";
}
private void Print(Object obj) {
//Console.Write(obj);
}
}
}