Example 4: Portfolio Optimization

Consider the portfolio problem

\[\min_{\omega_1, \ldots, \omega_n}\sum_{i=1}^{n}\Bigl(\rho(\omega_i)+\tfrac{1}{2}\omega_i^2\Bigr), \qquad \text{s.t.}\ \sum_{i=1}^n \omega_i =1, \quad \sum_{i=1}^n \omega_i r_i \geq \alpha,\]

where \(n\) is the number of assets, \(\omega_i\) is the weight of asset \(i\) (\(\omega_i<0\) short, \(\omega_i>0\) long), \(r_i\) is the expected return, \(\alpha\) is the minimum required portfolio return, and \(\rho\colon\mathbb{R}\to\mathbb{R}_{+}\) is a convex PLQ transaction cost.

Workflow matches the earlier examples, with two differences: the prototype loss is specified from points, and linear constraints are passed via clf._A and clf._b.

[1]:
import numpy as np
z = np.linspace(-1,1,1000)
interval0 = [1 if (-0.25<=i<=0.25) else 0 for i in z]
interval1 = [1 if (-0.5<=i<-0.25 or 0.25<i<=0.5) else 0 for i in z]
interval2 = [1 if (-0.75<=i<-0.5 or 0.5<i<=0.75) else 0 for i in z]
interval3 = [1 if (i<=-0.75 or i>=0.75) else 0 for i in z]
f = 0.4* (np.abs(z)) * interval0 + (0.8* (np.abs(z)-0.25)+0.1) * interval1 + (1.2* (np.abs(z)-0.5)+0.3) * interval2 + (1.6* (np.abs(z)-0.75)+0.6) * interval3
[2]:
import matplotlib.pyplot as plt

plt.figure(figsize=(4, 3))
plt.plot(z, f, label='Transaction cost')
plt.legend()
plt.xlabel('z')
plt.ylabel('c(z)')
plt.title('Transcation Cost')
plt.show()
../_images/notebooks_ex4_portfolio_2_0.png

1. Data Generation

\(n=10\) assets; C = 0.5 is the ReHLine ERM weight. With \(\mathbf{X}=\mathbf{I}_n\), coordinates correspond to portfolio weights \(\omega_i\).

[3]:
from plqcom import PLQLoss, plq_to_rehloss, affine_transformation
from rehline import ReHLine
[4]:
# Generate a portfolio dataset
n, C = 10, 0.5
np.random.seed(1024)
X = np.eye(n)
r = -0.5 +np.random.rand(10)
[5]:
X
[5]:
array([[1., 0., 0., 0., 0., 0., 0., 0., 0., 0.],
       [0., 1., 0., 0., 0., 0., 0., 0., 0., 0.],
       [0., 0., 1., 0., 0., 0., 0., 0., 0., 0.],
       [0., 0., 0., 1., 0., 0., 0., 0., 0., 0.],
       [0., 0., 0., 0., 1., 0., 0., 0., 0., 0.],
       [0., 0., 0., 0., 0., 1., 0., 0., 0., 0.],
       [0., 0., 0., 0., 0., 0., 1., 0., 0., 0.],
       [0., 0., 0., 0., 0., 0., 0., 1., 0., 0.],
       [0., 0., 0., 0., 0., 0., 0., 0., 1., 0.],
       [0., 0., 0., 0., 0., 0., 0., 0., 0., 1.]])
[6]:
r
[6]:
array([ 0.14769123,  0.49691358,  0.01880326,  0.15811273,  0.09906347,
        0.25306733, -0.36375287, -0.49588288, -0.35049112,  0.198439  ])

2. Create and Decompose the PLQ Loss

Define the transaction cost \(\rho\) from points (piecewise-linear on \(|z|\)):

[7]:
# Create a PLQLoss object
plqloss = PLQLoss(points=np.array([[-0.75,0.6],[-0.5,0.3],[-0.25,0.1],[0,0],[0.25,0.1],[0.5,0.3],[0.75,0.6]]),
                  form='points')

Decompose to ReLU–ReHU form with plq_to_rehloss:

[8]:
# Decompose transaction cost to ReLU–ReHU form
rehloss = plq_to_rehloss(plqloss)

3. Broadcast to All Samples

Here \(L_i(z_i)=L(z_i)\) (identity affine map: \(p=1\), \(q=0\), \(c_i=1\)).

C = 0.5 from step 1 is the ReHLine ERM weight only. Use c=1 in affine_transformation below — do not confuse c with ReHLine(C=C).

[9]:
# c=1: uniform weights; portfolio ERM strength via ReHLine(C=C) in step 4
rehloss = affine_transformation(rehloss, n=X.shape[0], c=1, p=1, q=0)
[10]:
print(rehloss.relu_coef.shape)
print("First ten sample relu coefficients: %s" % rehloss.relu_coef[0][:10])
print("First ten sample relu intercepts: %s" % rehloss.relu_intercept[0][:10])
(6, 10)
First ten sample relu coefficients: [-0.2 -0.2 -0.2 -0.2 -0.2 -0.2 -0.2 -0.2 -0.2 -0.2]
First ten sample relu intercepts: [0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]

4. Solve with ReHLine

Pass decomposed coefficients plus linear constraints clf._A, clf._b, then call fit(X).

[11]:
A = np.array([r,np.ones(10)])
b = np.array([-0.3,-1])
clf = ReHLine(C=C)
clf._U, clf._V, clf._A, clf._b = rehloss.relu_coef, rehloss.relu_intercept, A, b
clf.fit(X=X)
print('sol provided by rehline: %s' % clf.coef_)
sol provided by rehline: [ 1.34941891e-01  2.54603696e-01  1.69638746e-02  1.44481245e-01
  9.04303147e-02  2.31398247e-01  2.69356477e-18 -5.41557530e-02
 -4.84502846e-19  1.81394027e-01]