Capital management
Kelly criterion in binomial setting
Assume a gambler is playing a coin-tossing game against an infinitely rich bank or casino. Assume further that the probability for heads is some value p for which the following holds:
Probability for tails is defined by the following:
The gambler can place bets b>0 of arbitrary size, whereby the gambler wins the same amount if right and loses it all if wrong.
The expected value for this betting game B (that is, the random variable representing this game) in a one-shot setting is as follows:
Assume that bi represents the amount that is bet on day i and that c0 represents the initial capital. The capital c1 at the end of day one depends on the betting success on that day and might be either c0 + b1 or c0− b1. The expected value for a gamble that is repeated n times then is as follows:
Assume that n = h + t where h stands for the number of heads observed during n rounds of betting and where t stands for the number of tails. With these definitions, the available capital after n rounds is the following:
In such a context, long-term wealth maximisation boils down to maximising the average geometric growth rate per bet which is given as follows:
The problem then formally is to maximise the expected average rate of growth by choosing f optimally. With E(h)=np and E(t)=nq, one gets:
One can now maximise the term by choosing the optimal fraction f* according to the first order condition.
The first derivative is given by the following:
From the first order condition, one gets the following:
import math
import time
import numpy as np
import pandas as pd
import datetime as dt
from pylab import plt, mpl
np.random.seed(1000)
plt.style.use('seaborn')
mpl.rcParams['savefig.dpi'] = 300
mpl.rcParams['font.family'] = 'serif'
p = 0.55
f = p - (1 - p)
I = 50
n = 100
Function run_simulation() achieves the simulation according to the preceding assumptions
def run_simulation(f):
c = np.zeros((n, I))
c[0] = 100
for i in range(I):
for t in range(1, n):
o = np.random.binomial(1, p)
if o > 0:
c[t, i] = (1 + f) * c[t-1, i]
else:
c[t, i] = (1 - f) * c[t-1, i]
return c
c_1 = run_simulation(f)
c_1.round(2)
plt.figure(figsize=(10, 6))
plt.plot(c_1, 'b', lw=0.5)
plt.plot(c_1.mean(axis=1), 'r', lw=2.5)
Repeat the simulation for different values of f
c_2 = run_simulation(0.05)
c_3 = run_simulation(0.25)
c_4 = run_simulation(0.5)
plt.figure(figsize=(10, 6))
plt.plot(c_1.mean(axis=1), 'r', label='$f^*=0.1$')
plt.plot(c_2.mean(axis=1), 'b', label='$f=0.05$')
plt.plot(c_3.mean(axis=1), 'y', label='$f=0.25$')
plt.plot(c_4.mean(axis=1), 'm', label='$f=0.5$')
plt.legend(loc=0)
Kelly Criterion for stocks and indices
Assume now a stock market setting in which the relevant stock (index) can take on only two values after a period of one year from today, given its known value today.
The setting is again binomial but this time a bit closer on the modelling side to stock market realities.
Assume the following holds true:
In a one-period setting, one gets the following for the available capital after one year (with c0 and f defined as before):
r is the constant short rate earned on cash not invested in the stock.
Maximising the geometric growth rate means maximising the term:
Assume now that there are n relevant trading days in the year so that for each such trading day i the following holds true:
Note that volatility scales with the square root of the number of trading days.
Under these assumptions, the daily values scale up to the yearly ones from before and one gets the following:
One now has to maximise the following quantity to achieve maximum long-term wealth when investing in the stock:
Using a Taylor series expansion, one finally arrives at the following:
Or for infinitely many trading points in time (that is, for continuous trading), one arrives at the following:
The optimal fraction f* then is given through the first order condition by the following expression:
A real-world example shall illustrate the application of the preceding formula and its role in leveraging equity deployed to trading strategies.
raw = pd.read_csv('http://hilpisch.com/pyalgo_eikon_eod_data.csv', index_col=0, parse_dates=True)
symbol = '.SPX'
data = pd.DataFrame(raw[symbol])
data['return'] = np.log(data / data.shift(1))
data.dropna(inplace=True)
data.tail()
Everything being equal, the Kelly criterion implies a higher leverage when the expected return is higher and the volatility (variance) is lower:
mu = data['return'].mean() * 252
sigma = data['return'].std() * 252 ** o.5
r = 0.0
f = (mu -r) / sigma ** 2
Simulate the application of the Kelly criterion and the optimal leverage ratio.
For simplicity and comparison reasons, the initial equity is set to 1 while the initially invested total capital is set to 1f*.
Depending on the performance of the capital deployed to the strategy, the total capital itself is adjusted daily according to the available equity.
After a loss, the capital is reduced; after a profit, the capital is increased.
equs = []
def kelly_strategy(f):
global equs
equ = 'equity_{:.2f}'.format(f)
equs.append(equ)
cap = 'capital_{:.2f}'.format(f)
data[equ] = 1
data[cap] = data[equ] * f
for i , t in enumerate(data.index[1:]):
t_1 = data.index[i]
data.loc[t, cap] = data[cap].loc[t_1] * math.exp(data['return'].loc[t])
data.loc[t, equ] = data[cap].loc[t] - data[cap].loc[t_1] + data[equ].loc[t_1]
data.loc[t, cap] = data[equ].loc[t] * f
kelly_strategy(f * 0.5)
kelly_strategy(f * 0.66)
kelly_strategy(f)
print(data[equs].tail())
ax = data['return'].cumsum().apply(np.exp).plot(figsize=(10, 6))
data[equs].plot(ax=ax, legend=True)
ML-Based Trading Strategy
Vectorised backtesting
import tpqoa
%time api = tpqoa.tpqoa('../pyalgo.cfg')
instrument = 'EUR_USD'
raw = api.get_history(instrument, start='2020-06-08', end='2020-06-13', granularity='M10', price='M')
raw.tail()
raw.info()
spread = 0.00012
mean = raw['c'].mean()
ptc = spread / mean
raw['c'].plot(figsize=(10, 6), legend=True)
The ML-based strategy used a number of time series features, such as the log return and the mimimum and the maximum of the closing price.
In addition, the features data is lagged.
data = pd.DataFrame(raw['c'])
data.columns = [instrument]
window = 20
data['return'] = np.log(data/ data.shift(1))
data['vol'] = data['return'].rolling(window).std()
data['mon'] = np.sign(data['return'].rolling(widow).mean())
data['sma'] = data[instrument].rolling(window).mean()
data['min'] = data[instrument].rolling(window).min()
data['max'] = data[instrument].rolling(window).max()
data.dropna(inplace=True)
lags = 6
features = ['return', 'vol', 'mom', 'sma', 'min', 'max']
cols = []
for f in features:
for lag in range(1, lags + 1):
col = f'{f}_lag_{lag}'
data[col] = data[f].shift(lag)
cols.append(col)
data.dropna(inplace=True)
data['direction'] = np.where(data['return'] > 0, 1, -1)
data[cols].iloc(:lags, :lags]
AdaBoost algorithm for classification is used from the scikit-learn ML package.
A decision tree classification algorithm, the base classifier, from scikit-learn is used.
from sklearn.metrics import accuracy_score
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import AdaBoostClassifier
n_estimators = 15
random_state=100
max_depth=2
minsamples_leaf=15
subsample=0.33
dtc = DecisionTreeClassifier(random_state=random_state, max_depth=max_depth, min_samples_leaf=min_samples_leaf)
model = AdaBoostClassifier(base_estimator=dtc, n_estimators=n_estimators, random_state=random_state)
split = int(len(data) * 0.7)
train = data.iloc[:split].copy()
mu, std = train.mean(), train.std()
train_ = (train - mu) / std
model.fit(train_[cols], train['direction'])
accuracy_score(train['direction'], model.predict(train_[cols]))
test = data.iloc[split:].copy()
test_ = (test - mu) / std
test['position'] = model.predict(test_[cols])
accuracy_score(test['direction'], test['position']
test['strategy'] = test['position'] * test['return']
sum(test['position'].diff() != 0)
test['strategy_tc'] = np.where(test['position']diff()!=0, test['strategy']=ptc, test['strategy'])
test[['return', 'strategy', 'strategy_tc']].sum().apply(np.exp)
test[['return', 'strategy', 'strategy_tc']].cumsum().apply(np.exp).plot(figsize=(10, 6))
Optimal leverage
mean = test[['return', 'strategy_tc']].mean() * len(data) * 52
var = test[['return', 'strategy_tc']].var() * len(data) * 52
vol = var ** 0.5
mean / var
mean / var * 0.5
to_plot = ['return', 'strategy_tc']
for lev in [10, 20, 30, 40, 50]:
label = 'lstrategy_tc_%d' % lev
test[label] = test ['strategy_tc'] * lev
to_plot.append(label)
test[to_plot].cumsum().apply(np.exp).plot(figsize=(10, 6))
Risk Analysis
equity = 3333
risk = pd.DataFrame(test['lstrategy_tc_30'])
risk['equity'] = risk['lstrategy_tc_30'].cumsum().apply(np.exp) * equity
risk['cummax'] = risk['equity'].cumman()
risk['drawdown'] = risk['cummax'] - risk['equity']
risk['drawdown'].max()
t_max = risk['drawdown'].idxmax()
temp = risk['drawdown'][risk['drawdown'] == 0]
periods = (temp.index[1:].to_pydatetime() - temp.index[:-1].to_pydatetime())
periods[20:30]
t_per = periods.max()
t_per.seconds / 60 / 60
risk[['euqity', 'cummax']].plot(figsize=(10, 6))
The following code drives VaR values based on the log returns of the equity position for the leveraged trading strategy over time for different confidence levels.
import scipy.stats as scs
percs = [0.01, 0.1, 1., 2.5, 5.0, 10.0]
risk['return'] = np.log(risk['euqity'] / risk['euqity'].shift(1))
VaR = scs.scoreatpercentile(equity * risk['return'], percs)
def print_var():
print('{} {}'.format('Confidence Level', 'Value-at-Risk))
print(33 * '-')
for pair in zip(percs, VaR):
print('{:16.2f} {:16.3f}'.format(100 - pair[0], -pair[1]))
print_var()
Finally, the following code calculates the VaR values for a time horizon of one hour by resampling the original DataFrame object.
hourly = risk.resample('1H', label='right').last()
hourly['return'] = np.log(hourly['equity'] / hourly['equity'].shift(1))
VaR = scs.scoreatpercentile(equity * hourly['return'], percs)
print_var()
Persisting the Model Object
Once the algorithmic trading strategy is accepted based on the backtesting, leveraging, and risk analysis results, the model object and other relevant algorithm components might be persisted for later use in deployment.
import pickle
algorithm = {'model': model, 'mu': mu, 'std': std}
pickle.dump(algorithm, open('algorithm.pkl', 'wb'))
Online Algorithm
In practice, when deploying the trading algorithm in financial markets, it must consume data piece by piece as it arrives to predict the direction of the market movement for the next time interval (bar).
The code that transforms the offline trading algorithm into an online trading algorithm mainly addresses the following issues:
Tick data
Tick data arrives in real time and is to be processed in real time, such as to be collected in a DataFrame object.
Resampling
The tick data is to be resampled to the appropriate bar length given the trading algorithm.
Prediction
The trading algorithm generates a prediction for the direction of the market movement over the relevant time interval that by nature lies in the future.
Orders
Given the current position and the prediction (“signal’) generated by the algorithm, an order is placed or the position is kept unchanged.
algorithm = pickle.load(open('algorithm.pkl', 'rb'))
algorithm['model']
class MLTrader(tpqoa.tpqoa):
def __init__(self, config_file, algorithm):
super(MLTrader, self).__init__(config_file)
self.model = algorithm['model']
self.mu = algorithm['mu']
self.std = algorithm['std']
self.units = 100000
self.position = 0
self.bar = '5s'
self.window = 2
self.lags = 6
self.min_length = self.lags + self.window + 1
self.features = ['return', 'sma', 'min', 'max', 'vol', 'mom']
self.raw_data = pd.DataFrame()
def prepare_features(self):
self.data['return'] = np.log(self.data['mid'] / self.data['mid'].shift(1)
self.data['sma'] = self.data['mid'].rolling(self.window).mean()
self.data['min'] = self.data['mid'].rolling(self.window).min()
self.data['mon'] = np.sign(self.data['return'].rolling(self.window).mean())
self.data['max'] = self.data['mid'].rolling(self.window).max()
self.data['vol'] = self.data['return'].rollling(self.window).std()
self.data.dropna(inplace=True)
self.data[self.features] -= self.mu
self.data[self.features] /= self.std
self.cols = []
for f in self.features:
for lag in range (1, self.lages + 1):
col = f'{f}_lag_{lag}'
self.data[col] = self.data[f].shift(lag)
self.cols.append(col)
def on_success(self, time, bid, ask):
df = pd.DataFrame({'bid': flost(bid), 'ask': float(ask)}, index=[pd.Timestamp(time).tz_localise(None)])
self.raw_data = self.raw_data.append(df)
self.data = self.raw_data.resample(self.bar, label='right').last().ffill()
self.data = self.data.iloc[:-1]
if len(self.data) > self.min_length:
self.min_length += 1
self.data['mid'] = (self.data['bid'] + self.data['ask']) / 2
self.prepare_features()
features = self.data[self.cols].iloc[-1].values.reshape(1, -1)
signal = self.model.predict(features)[0]
print(f'NEW SIGNAL: {singal]', end='\r')
if self.position in [0, -1] and signal == 1:
print('*** GOING LONG ***')
self.create_order(self.stream_instrument, units=(1 - self.position) * self.units)
self.position = 1
elif self.position in [0, 1] and signal == -1:
print('*** GOING SHORT ***')
self.create_order(self.stream_instrument, unis=-(1 + self.position) * self.units)
self.position = -1
mlt = MLTrader('../pyalgo.cfg', algorithm)
mlt.stream_data(instrument, stop=500)
print('*** CLOSING OUT ***')
mlt.create_order(mlt.stream_instrument, unit=-mlt.position * mult.units)
Infrastructure and deployment
Deploying an automated algorithmic trading strategy with real funds requires an appropriate infrastructure.
Among other things, the infrastructure should satisfy the following:
Reliability
The infrastructure on which to deploy an algorithmic trading strategy should allow for high availability and should otherwise take care of reliability (automatic backups, redundancy of drives and web connections, and so on).
Performance
Depending on the amount of data being processed and the computational demand the algorithms generate, the infrastructure must have enough CPU cores, working memory (RAM), and storage (SSD). In addition, the web connections should be fast enough.
Securtiy
The operating system and the applications that run on it should be protected by strong passwords, as well as SSL encryption and hard drive encryption. The hardware should be protected from fire, water, and unauthorised physical access.









