%matplotlib inline
from IPython.core.display import HTML
import numpy as np
import matplotlib
import matplotlib.pyplot as plt
import pandas as pd
from pandas import DataFrame, Series
matplotlib.rcParams.update({'font.size': 16})
The Iowa Electronic Markets are small, real-money financial markets designed to aggregate information about future events. The market microstructure of these markets is studied and a market making model is developed to provide liquidity for one set of securities offered by this exchange. A computer program was created to employ the market making model and profit from the market's inefficiencies. Using invested capital, the system traded 34% of the total market volume and achieved a Sharpe ratio of 9.9. This paper reveals the details of how this algorithmic trader worked to show how it functioned and the value it added to the Iowa Electronic Markets.
Price movements must be inversely correlated because when the value of one security goes up, the other must go down
securities = one of the two securities representing the two candidates
A long position in the market asset is defined as a long position in the Democratic candidate and a short position in the Republican candidate, and a short position in the market asset is the opposite.
Since there are two securities that are by definition inversely correlated, it simplifies things to invert the price of one of them so it is comparable to the other. For example, if these are the market prices:
# price diagram code
def price_diagram(data, labels, colors, xsteps):
sec1 = [(xsteps[0], data[0, 0] - xsteps[0]), (data[0, 1], xsteps[1] - data[0, 1])]
sec2 = [(xsteps[0], data[1, 0] - xsteps[0]), (data[1, 1], xsteps[1] - data[1, 1])]
xlabels = np.arange(xsteps[0], xsteps[1] + 0.001, xsteps[2])
xlim = np.arange(xsteps[0], xsteps[1], xsteps[2])
fig, ax = plt.subplots()
fig.set_size_inches((10, 1.5))
ax.broken_barh(sec1, (21, 8), facecolors=colors[1], alpha=0.5)
ax.broken_barh(sec2, (11, 8), facecolors=colors[0], alpha=0.5)
ax.set_ylim(5,35)
ax.set_xlim(*xsteps[:2])
ax.set_xticks(xlabels)
ax.set_xticklabels(xlabels)
ax.set_xlabel('Price')
ax.set_yticks([15,25])
ax.set_yticklabels(labels)
ax.grid(True)
return HTML('<center>' + DataFrame(data,
index=reversed(labels),
columns=['BID', 'ASK']).to_html() +
'</center>')
def DEM_REP(data, xsteps=(0, 1, 0.1)):
return price_diagram(np.array(data),
labels=['REP', 'DEM'],
colors=['red', 'blue'],
xsteps=xsteps)
def DEM_REPinv(data, xsteps=(0, 1, 0.1)):
data = np.array(data)
data_inv = np.array([data[0, :], (1 - data[1, 1], 1 - data[1, 0])])
return price_diagram(data_inv,
labels=['REPinv', 'DEM'],
colors=['red', 'blue'],
xsteps=xsteps)
def Good_Bad(data, xsteps=(0, 1, 0.1)):
data = np.array(data)
data_inv = np.array([data[0, :], (1 - data[1, 1], 1 - data[1, 0])])
data_gb = np.array([(max(data_inv[:, 0]), min(data_inv[:, 1])),
(min(data_inv[:, 0]), max(data_inv[:, 1]))])
if data_gb[0, 0] < data_gb[0, 1]:
return price_diagram(data_gb,
labels=['Bad spread', 'Good spread'],
colors=['brown', 'green'],
xsteps=xsteps)
else:
return price_diagram(data_gb,
labels=['Bad', 'Good spread (crossed)'],
colors=['brown', 'green'],
xsteps=xsteps)
DEM_REP([(0.572, 0.602), (0.424, 0.45)], xsteps=(0.3, 0.7, 0.05))
BID | ASK | |
---|---|---|
DEM | 0.572 | 0.602 |
REP | 0.424 | 0.450 |
Subtracting the REP bid-ask prices from 1 yields bid-ask prices for REPinv:
DEM_REPinv([(0.572, 0.602), (0.424, 0.45)], xsteps=(0.5, 0.7, 0.05))
BID | ASK | |
---|---|---|
DEM | 0.572 | 0.602 |
REPinv | 0.550 | 0.576 |
REPinv has the same market exposure as DEM.
Notice that the REP bid price determines the REPinv ask price, and the REP ask price determines the REPinv bid price.
If a trader wanted to trade in this market it would be efficient and rational for them to only trade using the asset's true bid and ask prices. These two prices make up what can be considered to be the good spread and the other two prices make up the bad spread.
Good_Bad([(0.572, 0.602), (0.424, 0.45)], xsteps=(0.5, 0.7, 0.05))
BID | ASK | |
---|---|---|
Good spread | 0.572 | 0.576 |
Bad spread | 0.550 | 0.602 |
This pricing methodology does not allow arbitrage, but when the good spread is crossed (the bid is greater than the ask), the fair price is undefined.
report = pd.read_csv("daily_report.csv")
report.set_index("Date", inplace=True)
rollingwindow = 7
sumRR = report.sumRR.cumsum().diff(rollingwindow)
sumR = report.sumR.cumsum().diff(rollingwindow)
sigma=(sumRR/(rollingwindow*96) - (sumR/(rollingwindow*96))**2)**0.5 * 96**0.5 * 100
report['Volatility'] = sigma
report["Total Profit"] = report["Total Profit"].cumsum()
report["MM Profit"] = report["MM Profit"].cumsum()
report["Arb Profit"] = report["Arb Profit"].cumsum()
report["Spread Profit"] = report["Spread Profit"].cumsum()
report["Positioning Profit"] = report["Positioning Profit"].cumsum()
def activity_plot(fields, styles):
fig, ax = plt.subplots()
fig.set_size_inches((12, 8))
for field, style in zip(fields, styles):
report[field].plot(style=style, label=field)
ax.set_xlabel('Date')
ax.grid(True)
plt.legend(loc=2)
Plots of Market Activity during the time of this research project.
activity_plot(["Mid Price"], ["r"])
activity_plot(["Volatility"], ["r"])
activity_plot(["Good Spread", "Bad Spread"], ["green", "brown"])
activity_plot(["Total Market Value", "My $ Traded"], ["k", "r"])
For example, in this market:
DEM_REP([(0.572, 0.602), (0.424, 0.45)], xsteps=(0.3, 0.7, 0.05))
BID | ASK | |
---|---|---|
DEM | 0.572 | 0.602 |
REP | 0.424 | 0.450 |
DEM_REPinv([(0.572, 0.602), (0.424, 0.45)], xsteps=(0.3, 0.7, 0.05))
BID | ASK | |
---|---|---|
DEM | 0.572 | 0.602 |
REPinv | 0.550 | 0.576 |
A DEM bid of 0.580 would have this result:
DEM_REPinv([(0.58, 0.602), (0.424, 0.45)], xsteps=(0.3, 0.7, 0.05))
BID | ASK | |
---|---|---|
DEM | 0.58 | 0.602 |
REPinv | 0.55 | 0.576 |
Good_Bad([(0.58, 0.602), (0.424, 0.45)], xsteps=(0.3, 0.7, 0.05))
BID | ASK | |
---|---|---|
Good spread (crossed) | 0.58 | 0.576 |
Bad | 0.55 | 0.602 |
The arbitrage opportunity is to sell both the DEM and REP securities at their respective bid prices and then buy bundles by trading with the exchange.
DEM_REP([(0.58, 0.602), (0.424, 0.45)], xsteps=(0.3, 0.7, 0.05))
BID | ASK | |
---|---|---|
DEM | 0.580 | 0.602 |
REP | 0.424 | 0.450 |
This is profitable because the pair of securities can be sold at a price of 1.004 and bought back from the exchange with a bundled transaction for 1 dollar. The exchange has a special market order for buying and selling bundles using the sum of the displayed bid or ask prices that ensures equal numbers of each security are traded.
Consider an exponential utility function to evaluate changes in wealth, W:
$$U(W) = -e^{-\lambda W}, \lambda > 0$$Assume that $W$ is distributed normally with a mean of $\mu$ and a variance of $\sigma^2$. The expectation of $U(W)$ is then given by:
$$\mathbf{E}U(W) = \frac{1}{\sigma \sqrt{2\pi}} \int_{-\infty}^{\infty} -e^{-\lambda W} e^{-\frac{(W-u)^2}{2\sigma^2}} dW$$By re-arranging the terms and employing calculus, one can reduce this to:
$$\mathbf{E}U(W) = -e^{-\lambda (\mu - \frac{\lambda \sigma^2}{2})}$$Maximizing the expected utility of changes in wealth $W$ is then equivalent to maximizing the expression
$$\mu - \frac{\lambda \sigma^2}{2}$$The exponential utility function is equivalent to the mean variance utility function when changes in wealth are normally distributed.
We can then write the market making model's utility function as:
$$U(W) = \mathbf{E}[W] - \frac{\lambda}{2} \mathbf{Var}[W]$$The market maker can estimate the expected increase in wealth when it buys a security on its bid price or sells at its ask price:
$$\mathbf{E}[W] = s \left | X \right | - G(Z)(X+A-D)$$where:
If:
Then:
function $G(Z)$ becomes an estimate of the price change $P_{post} - P$:
$$G(Z) = P_{post} - P$$The function $G(Z)$ must be linear in $Z$ to enforce a no arbitrage condition on the market.
The linear market impact model is:
$$\frac{P_{post} - P}{P} = \gamma Z$$Market impact is then a linear function of the constant $\gamma$.
Defining $Y=Z-X$ as the estimated size of the trade filled by other market participants at the same price, the market impact function $G(Z)$ becomes
$$G(Z) = \gamma P (X + Y)$$The estimate of the expected profit from a buy or sell trade is:
$$\mathbf{E}[W] = s \left | X \right | - \gamma P (X + Y)(X+A-D)$$The variance of the expected increase in wealth comes from the variance of changes in the undesired position value, including the new position $X$. The market making model can estimate the variance of net wealth as:
$$\mathbf{Var}[W] = (\sigma \sqrt{t} P(X+A-D))^2$$where:
Substituting $\mathbf{E}[W]$ and $\mathbf{Var}[W]$ into the utility function $U(W) = \mathbf{E}[W] - \frac{\lambda}{2} \mathbf{Var}[W]$ yields:
$$\mathbf{U}(W) = sX - \gamma P (X + Y)(A-D+X) - \frac{\lambda}{2} (\sigma \sqrt{t} P(A-D+X))^2$$To find the optimal number of shares the market maker would be willing to buy or sell at a price earning a spread of $s$, simply calculate the first derivative of $\mathbf{U}(W)$, set the result equal to zero, and solve for $X$.
The optimal number of shares for the market maker to be willing to buy on the bid or sell at the offer becomes:
$$X_{bid} = \max\Biggl[0, \frac{s - \gamma P (Y + A - D) - \lambda \sigma^2 t P^2 (A - D)}{2 \gamma P + \lambda \sigma^2 t P^2}\Biggr]\\ X_{ask} = \max\Biggl[0, \frac{s - \gamma P (Y - A + D) + \lambda \sigma^2 t P^2 (A - D)}{2 \gamma P + \lambda \sigma^2 t P^2}\Biggr]$$Interactive Model to help visualize the model available here
activity_plot(["Total Profit", "MM Profit", "Arb Profit"], ["k", "r", "b"])
activity_plot(["MM Trades", "Arb Attempts"], ["k", "r"])
Which will be greater: spread profits or positioning profits?
activity_plot(["MM Profit", "Spread Profit", "Positioning Profit"], ["r", "k--", "b--"])