The Kelly ratio gives the optimal percentage of an investor’s wealth to bet in order to maximize his long term growth rate. It was originally developed for betting with known odds and win ratios, for games such as blackjack and horse betting. However, it has its uses in investments as well. For a stock instrument, the Kelly ratio, k, is given as k = (μ – r) / σ2 , where μ is a stock’s average return, r is the risk free rate, and σ2 is the stock’s variance. For full details, please refer to Ed Thorp’s more detailed paper.
We’ll take a look at a modified Kelly ratio, with μ and σ2 calculated over a moving window, and run a strategy where we invest this portion of our wealth. Lastly, we take a detailed look into various factors that might impact performance.
This workflow assumes a basic understanding of the Custom Metric and Strategy applications. For a general introduction or a quick refresher, please refer to the Palantir Finance Learning Center.
Note: This workflow requires US3M data, currently not available on Joyride.
The video below is best viewed in full screen mode on the highest quality.
If you have trouble viewing the embedded video, you can download a copy here.
Implementation Code
Custom Metric:
TimeSeriesBuilder kellyValues = createTimeSeriesBuilder();
// TimeSeries with relevant data
TimeSeries averageChange = this.percentChange().ma(trailingPoints);
TimeSeries varianceChange = this.percentChange().variance(trailingPoints);
TimeSeries riskless = US3M/25200;
// Dates to calculate the Kelly ratio on. We'll need valid TimeSeries data and riskless rate (US3M) data.
DateSet span1 = this.startDate.to(this.endDate());
DateSet span2 = US3M.startDate.to(US3M.endDate());
DateSet iterateDates = span1.intersect(span2);
for (date d : iterateDates) {
// Before there is enough data to make an accurate measurement for the first set of moving averages, invest nothing
if (d < this.lag(trailingPoints).startDate()) {
kellyValues.addTick(d, 0);
} else {
Number meanReturn = averageChange.valueOn(d);
Number risklessRate = (US3M/25200).valueOn(d);
Number variance = varianceChange.valueOn(d);
// Moving Kelly ratio for date d
kellyValues.addTick(d, ((meanReturn - risklessRate) / variance));
}
}
return kellyValues.toTimeSeries();
Strategy:
Setup:
// TimeSeries/Instrument to invest in
TimeSeries invest = SPX;
// Patches together US3M using slice and stitch
Date sliceDate = '1986-08-14';
TimeSeries slice1 = US3M.slice(US3M.startDate().to(sliceDate)) / 5200;
TimeSeries slice2 = US3M.slice(sliceDate.to(US3M.endDate())) / 25200;
TimeSeries riskless = slice1.stitch(slice2).percentReturnToLevel();
// Calculation of the Kelly ratio as a TimeSeries
Integer trailing = 4*252;
TimeSeries kellyValues = invest.movingKelly(trailing);
// Rebalances everyday in this DateSet
DateSet restrictDates = DateSet("Fridays");
// Precomputes dates to run faster
DateSet runDates = restrictDates.intersect(kellyValues.validDates()).intersect(strategyStartDate.to(strategyEndDate)).precompute();
// Weighting factor for the Kelly coefficient
Number kellyCoeff = 1.0;
Loop:
// Skip every date not in our run dates
if (runDates.containsNow()) {
// Exit if we go bankrupt
if (portfolio.nav().value() < 0) {
quit("Bankrupt");
}
Number k = kellyValues.value() * kellyCoeff;
print("adjusted kelly: " + k);
// Hold the percentage of our wealth in our asset, as indicated by the Kelly ratio
// If k is negative, we short our asset, and if it is above 1, we are leveraged.
hold(invest,(k * 100),enum.PERCENT_OF_NAV);
// Hold the rest in the riskless asset. We can simulate borrowing at this rate by shorting the riskless asset
hold(riskless,100 - (k * 100),enum.PERCENT_OF_NAV);
// Debug: Prints information from each cycle for the strategist to view. No impact on strategy run.
print(invest + " contracts: " + numContracts(invest));
print(invest + " price: " + invest.valueOn(now));
print("risk free contracts: " + numContracts(riskless));
print("risk free price: " + riskless.valueOn(now));
print("NAV: " + portfolio.nav().value());
}