Example 4: Portfolio Optimization¶
Consider the portfolio problem
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()
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]