OLMAR Online Portfolio Rebalancing with Moving Average Reversion
Summary
The document implements Online Portfolio Moving Average Reversion (OLMAR), a portfolio strategy that adjusts asset weights using relative moving-average prices. For each stock, it divides the window’s average price by the current price, then compares each asset’s relative value with the cross-asset average. A step size uses the current portfolio’s expected return, a target parameter, and the dispersion of those relative deviations to update weights.
The updated weights are projected onto the nonnegative unit simplex, keeping allocations long-only and summing to one. The algorithm then calculates target share amounts from portfolio value and current prices and submits trades to reach them. The example begins with equal weights and includes a seven-stock universe, a five-day default window, and a portfolio-value plot. It is an implementation example rather than an evaluation: it gives no performance results, and its fixed universe, trading assumptions, and unexamined transaction costs limit what can be inferred about live suitability.
Key ideas
- OLMAR estimates relative price deviations using moving averages divided by current prices.
- Portfolio weights shift according to the deviation from the cross-asset mean and a target parameter.
- Simplex projection keeps weights nonnegative and makes their sum equal one.
- Target holdings are calculated from desired weights, portfolio value, and current prices.
- The code illustrates a strategy implementation but does not report performance evidence.
Tags
Full text
# olmar.py
```py
import logging
import sys
import numpy as np
from zipline.finance import commission, slippage
# zipline_logging = logging.getLogger("zipline_logging")
# zipline_logging.addHandler(logging.NullHandler())
# zipline_logging.addHandler(
# logging.StreamHandler(sys.stdout).setLevel(logging.INFO),
# )
# zipline_logging.addHandler(logging.StreamHandler(sys.stderr).setLevel(logging.ERROR))
STOCKS = ["AMD", "CERN", "COST", "DELL", "GPS", "INTC", "MMM"]
# On-Line Portfolio Moving Average Reversion
# More info can be found in the corresponding paper:
# http://icml.cc/2012/papers/168.pdf
def initialize(algo, eps=1, window_length=5):
algo.stocks = STOCKS
algo.sids = [algo.symbol(symbol) for symbol in algo.stocks]
algo.m = len(algo.stocks)
algo.price = {}
algo.b_t = np.ones(algo.m) / algo.m
algo.last_desired_port = np.ones(algo.m) / algo.m
algo.eps = eps
algo.init = True
algo.days = 0
algo.window_length = window_length
algo.set_commission(commission.PerShare(cost=0, min_trade_cost=1.0))
algo.set_slippage(slippage.VolumeShareSlippage())
def handle_data(algo, data):
algo.days += 1
if algo.days < algo.window_length:
return
if algo.init:
rebalance_portfolio(algo, data, algo.b_t)
algo.init = False
return
m = algo.m
x_tilde = np.zeros(m)
# find relative moving average price for each asset
mavgs = data.history(algo.sids, "price", algo.window_length, "1d").mean()
for i, sid in enumerate(algo.sids):
price = data.current(sid, "price")
# Relative mean deviation
x_tilde[i] = mavgs[sid] / price
###########################
# Inside of OLMAR (algo 2)
x_bar = x_tilde.mean()
# market relative deviation
mark_rel_dev = x_tilde - x_bar
# Expected return with current portfolio
exp_return = np.dot(algo.b_t, x_tilde)
weight = algo.eps - exp_return
variability = (np.linalg.norm(mark_rel_dev)) ** 2
# test for divide-by-zero case
if variability == 0.0:
step_size = 0
else:
step_size = max(0, weight / variability)
b = algo.b_t + step_size * mark_rel_dev
b_norm = simplex_projection(b)
np.testing.assert_almost_equal(b_norm.sum(), 1)
rebalance_portfolio(algo, data, b_norm)
# update portfolio
algo.b_t = b_norm
def rebalance_portfolio(algo, data, desired_port):
# rebalance portfolio
desired_amount = np.zeros_like(desired_port)
current_amount = np.zeros_like(desired_port)
prices = np.zeros_like(desired_port)
if algo.init:
positions_value = algo.portfolio.starting_cash
else:
positions_value = algo.portfolio.positions_value + algo.portfolio.cash
for i, sid in enumerate(algo.sids):
current_amount[i] = algo.portfolio.positions[sid].amount
prices[i] = data.current(sid, "price")
desired_amount = np.round(desired_port * positions_value / prices)
algo.last_desired_port = desired_port
diff_amount = desired_amount - current_amount
for i, sid in enumerate(algo.sids):
algo.order(sid, diff_amount[i])
def simplex_projection(v, b=1):
r"""Projection vectors to the simplex domain
Implemented according to the paper: Efficient projections onto the
l1-ball for learning in high dimensions, John Duchi, et al. ICML 2008.
Implementation Time: 2011 June 17 by Bin@libin AT pmail.ntu.edu.sg
Optimization Problem: min_{w}\| w - v \|_{2}^{2}
s.t. sum_{i=1}^{m}=z, w_{i}\geq 0
Input: A vector v \in R^{m}, and a scalar z > 0 (default=1)
Output: Projection vector w
:Example:
>>> proj = simplex_projection([.4 ,.3, -.4, .5])
>>> proj # doctest: +NORMALIZE_WHITESPACE
array([ 0.33333333, 0.23333333, 0. , 0.43333333])
>>> print(proj.sum())
1.0
Original matlab implementation: John Duchi (jduchi@cs.berkeley.edu)
Python-port: Copyright 2013 by Thomas Wiecki (thomas.wiecki@gmail.com).
"""
v = np.asarray(v)
p = len(v)
# Sort v into u in descending order
v = (v > 0) * v
u = np.sort(v)[::-1]
sv = np.cumsum(u)
rho = np.where(u > (sv - b) / np.arange(1, p + 1))[0][-1]
theta = np.max([0, (sv[rho] - b) / (rho + 1)])
w = v - theta
w[w < 0] = 0
return w
def analyze(context=None, results=None):
import matplotlib.pyplot as plt
fig = plt.figure()
ax = fig.add_subplot(111)
results.portfolio_value.plot(ax=ax)
ax.set_ylabel("Portfolio value (USD)")
plt.show()
def _test_args():
"""Extra arguments to use when zipline's automated tests run this example."""
import pandas as pd
return {"start": pd.Timestamp("2004"), "end": pd.Timestamp("2008")}
```Shown in full with attribution under the source's licence. Licence: Apache-2.0
This summary was written by Stratmill's research agent from the original; it is not a copy of the source.