Skip to content

Commit 448b431

Browse files
committed
add more optimizers
1 parent 51ed976 commit 448b431

20 files changed

+548
-466
lines changed
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import tensorflow as tf
2+
import numpy as np
3+
import cvxpy as cp
4+
5+
class BlackLittermanOptimizer(tf.keras.layers.Layer):
6+
def __init__(self, n_assets, risk_aversion=2.5, tau=0.05):
7+
super(BlackLittermanOptimizer, self).__init__()
8+
self.n_assets = n_assets
9+
self.risk_aversion = risk_aversion
10+
self.tau = tau
11+
12+
def call(self, inputs):
13+
market_caps, Sigma, views, view_confidences = inputs
14+
15+
def black_litterman_optimization(market_caps, Sigma, views, view_confidences):
16+
# Calculate market equilibrium returns
17+
market_weights = market_caps / np.sum(market_caps)
18+
Pi = self.risk_aversion * Sigma @ market_weights
19+
20+
# Prepare views
21+
P = np.eye(self.n_assets)[views[:, 0].astype(int)]
22+
Q = views[:, 1]
23+
Omega = np.diag(1 / view_confidences)
24+
25+
# Black-Litterman formula
26+
BL_mean = np.linalg.inv(np.linalg.inv(self.tau * Sigma) + P.T @ np.linalg.inv(Omega) @ P) @ \
27+
(np.linalg.inv(self.tau * Sigma) @ Pi + P.T @ np.linalg.inv(Omega) @ Q)
28+
BL_cov = np.linalg.inv(np.linalg.inv(self.tau * Sigma) + P.T @ np.linalg.inv(Omega) @ P)
29+
30+
# Optimization
31+
w = cp.Variable(self.n_assets)
32+
risk = cp.quad_form(w, BL_cov)
33+
ret = BL_mean.T @ w
34+
objective = cp.Maximize(ret - self.risk_aversion * risk)
35+
constraints = [cp.sum(w) == 1, w >= 0]
36+
37+
prob = cp.Problem(objective, constraints)
38+
try:
39+
prob.solve(solver=cp.SCS)
40+
if prob.status != cp.OPTIMAL:
41+
raise ValueError('Optimization problem not solved optimally')
42+
return w.value
43+
except:
44+
return market_weights
45+
46+
optimized_w = tf.py_function(
47+
func=black_litterman_optimization,
48+
inp=[market_caps, Sigma, views, view_confidences],
49+
Tout=tf.float32
50+
)
51+
52+
return optimized_w
53+
54+
class BlackLittermanDiffOptPortfolio(tf.keras.Model):
55+
def __init__(self, input_dim, n_assets, hidden_dim, risk_aversion=2.5, tau=0.05):
56+
super(BlackLittermanDiffOptPortfolio, self).__init__()
57+
self.feature_extractor = tf.keras.Sequential([
58+
tf.keras.layers.Dense(hidden_dim, activation='relu', input_shape=(input_dim,)),
59+
tf.keras.layers.Dense(hidden_dim, activation='relu')
60+
])
61+
self.market_cap_predictor = tf.keras.layers.Dense(n_assets)
62+
self.sigma_predictor = tf.keras.layers.Dense(n_assets * n_assets)
63+
self.views_predictor = tf.keras.layers.Dense(n_assets * 2)
64+
self.view_confidence_predictor = tf.keras.layers.Dense(n_assets)
65+
self.bl_optimizer = BlackLittermanOptimizer(n_assets, risk_aversion, tau)
66+
67+
def call(self, inputs):
68+
features = self.feature_extractor(inputs)
69+
market_caps = tf.exp(self.market_cap_predictor(features)) # Ensure positive market caps
70+
sigma = tf.reshape(self.sigma_predictor(features), (-1, self.n_assets, self.n_assets))
71+
views = tf.reshape(self.views_predictor(features), (-1, self.n_assets, 2))
72+
view_confidences = tf.exp(self.view_confidence_predictor(features)) # Ensure positive confidences
73+
return self.bl_optimizer([market_caps, sigma, views, view_confidences])
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import tensorflow as tf
2+
import cvxpy as cp
3+
import numpy as np
4+
5+
class CVaROptimizer(tf.keras.layers.Layer):
6+
def __init__(self, n_assets, n_scenarios, alpha=0.95):
7+
super(CVaROptimizer, self).__init__()
8+
self.n_assets = n_assets
9+
self.n_scenarios = n_scenarios
10+
self.alpha = alpha
11+
12+
def call(self, inputs):
13+
returns_scenarios, = inputs
14+
15+
def solve_cvar_optimization(returns_scenarios):
16+
w = cp.Variable(self.n_assets)
17+
aux_var = cp.Variable(1)
18+
slack_vars = cp.Variable(self.n_scenarios)
19+
20+
portfolio_returns = returns_scenarios @ w
21+
objective = aux_var - (1 / (self.n_scenarios * (1 - self.alpha))) * cp.sum(slack_vars)
22+
23+
constraints = [
24+
cp.sum(w) == 1,
25+
w >= 0,
26+
slack_vars >= 0,
27+
slack_vars >= -portfolio_returns - aux_var
28+
]
29+
30+
prob = cp.Problem(cp.Maximize(objective), constraints)
31+
32+
try:
33+
prob.solve(solver=cp.SCS)
34+
if prob.status != cp.OPTIMAL:
35+
raise ValueError('Optimization problem not solved optimally')
36+
return w.value
37+
except:
38+
# Fallback to equal-weight portfolio if optimization fails
39+
return np.ones(self.n_assets) / self.n_assets
40+
41+
optimized_w = tf.py_function(
42+
func=solve_cvar_optimization,
43+
inp=[returns_scenarios],
44+
Tout=tf.float32
45+
)
46+
47+
return optimized_w
48+
49+
class CVaRDiffOptPortfolio(tf.keras.Model):
50+
def __init__(self, input_dim, n_assets, n_scenarios, hidden_dim, alpha=0.95):
51+
super(CVaRDiffOptPortfolio, self).__init__()
52+
self.feature_extractor = tf.keras.Sequential([
53+
tf.keras.layers.Dense(hidden_dim, activation='relu', input_shape=(input_dim,)),
54+
tf.keras.layers.Dense(hidden_dim, activation='relu')
55+
])
56+
self.returns_scenarios_generator = tf.keras.layers.Dense(n_assets * n_scenarios)
57+
self.cvar_optimizer = CVaROptimizer(n_assets, n_scenarios, alpha)
58+
59+
def call(self, inputs):
60+
features = self.feature_extractor(inputs)
61+
returns_scenarios = tf.reshape(self.returns_scenarios_generator(features), (-1, self.n_scenarios, self.n_assets))
62+
return self.cvar_optimizer([returns_scenarios])
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import tensorflow as tf
2+
import cvxpy as cp
3+
import numpy as np
4+
5+
class FactorNeutralOptimizer(tf.keras.layers.Layer):
6+
def __init__(self, n_assets, n_factors, factor_exposure_bounds=(-0.1, 0.1)):
7+
super(FactorNeutralOptimizer, self).__init__()
8+
self.n_assets = n_assets
9+
self.n_factors = n_factors
10+
self.factor_exposure_bounds = factor_exposure_bounds
11+
12+
def call(self, inputs):
13+
mu, Sigma, factor_exposures = inputs
14+
15+
def solve_factor_neutral_qp(mu, Sigma, factor_exposures):
16+
w = cp.Variable(self.n_assets)
17+
risk_aversion = cp.Parameter(nonneg=True)
18+
19+
objective = mu @ w - risk_aversion * cp.quad_form(w, Sigma)
20+
constraints = [
21+
cp.sum(w) == 1,
22+
w >= 0,
23+
factor_exposures @ w >= self.factor_exposure_bounds[0],
24+
factor_exposures @ w <= self.factor_exposure_bounds[1]
25+
]
26+
27+
prob = cp.Problem(cp.Maximize(objective), constraints)
28+
risk_aversion.value = 1.0 # Initial value for risk aversion
29+
30+
try:
31+
prob.solve(solver=cp.SCS)
32+
if prob.status != cp.OPTIMAL:
33+
raise ValueError('Optimization problem not solved optimally')
34+
return w.value
35+
except:
36+
# Fallback to equal-weight portfolio if optimization fails
37+
return np.ones(self.n_assets) / self.n_assets
38+
39+
optimized_w = tf.py_function(
40+
func=solve_factor_neutral_qp,
41+
inp=[mu, Sigma, factor_exposures],
42+
Tout=tf.float32
43+
)
44+
45+
return optimized_w
46+
47+
class FactorNeutralDiffOptPortfolio(tf.keras.Model):
48+
def __init__(self, input_dim, n_assets, n_factors, hidden_dim, factor_exposure_bounds=(-0.1, 0.1)):
49+
super(FactorNeutralDiffOptPortfolio, self).__init__()
50+
self.feature_extractor = tf.keras.Sequential([
51+
tf.keras.layers.Dense(hidden_dim, activation='relu', input_shape=(input_dim,)),
52+
tf.keras.layers.Dense(hidden_dim, activation='relu')
53+
])
54+
self.mu_predictor = tf.keras.layers.Dense(n_assets)
55+
self.sigma_predictor = tf.keras.layers.Dense(n_assets * n_assets)
56+
self.factor_exposures_predictor = tf.keras.layers.Dense(n_assets * n_factors)
57+
self.factor_neutral_optimizer = FactorNeutralOptimizer(n_assets, n_factors, factor_exposure_bounds)
58+
59+
def call(self, inputs):
60+
features = self.feature_extractor(inputs)
61+
mu = self.mu_predictor(features)
62+
sigma = tf.reshape(self.sigma_predictor(features), (-1, mu.shape[1], mu.shape[1]))
63+
factor_exposures = tf.reshape(self.factor_exposures_predictor(features), (-1, mu.shape[1], self.n_factors))
64+
return self.factor_neutral_optimizer([mu, sigma, factor_exposures])
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import tensorflow as tf
2+
import numpy as np
3+
import scipy.cluster.hierarchy as sch
4+
5+
class HierarchicalRiskParityOptimizer(tf.keras.layers.Layer):
6+
def __init__(self, n_assets):
7+
super(HierarchicalRiskParityOptimizer, self).__init__()
8+
self.n_assets = n_assets
9+
10+
def call(self, inputs):
11+
returns, = inputs
12+
13+
def hrp_optimization(returns):
14+
# Calculate correlation matrix
15+
corr = np.corrcoef(returns.T)
16+
17+
# Distance matrix
18+
dist = np.sqrt(0.5 * (1 - corr))
19+
20+
# Hierarchical clustering
21+
link = sch.linkage(dist, 'single')
22+
sortIx = sch.leaves_list(link)
23+
24+
# Sort correlation matrix
25+
corr = corr[sortIx, :][:, sortIx]
26+
27+
# Recursive bisection
28+
weights = np.ones(self.n_assets)
29+
clusters = [list(range(self.n_assets))]
30+
while len(clusters) > 0:
31+
clusters = [cl[start:end] for cl in clusters
32+
for start, end in ((0, len(cl) // 2), (len(cl) // 2, len(cl)))
33+
if len(cl) > 1]
34+
for i in range(0, len(clusters), 2):
35+
cl1 = clusters[i]
36+
cl2 = clusters[i + 1]
37+
var1 = np.sum(np.var(returns[:, cl1], axis=0))
38+
var2 = np.sum(np.var(returns[:, cl2], axis=0))
39+
alpha = 1 - var1 / (var1 + var2)
40+
weights[cl1] *= alpha
41+
weights[cl2] *= 1 - alpha
42+
43+
# Revert to original order
44+
weights = weights[np.argsort(sortIx)]
45+
return weights / np.sum(weights)
46+
47+
optimized_w = tf.py_function(
48+
func=hrp_optimization,
49+
inp=[returns],
50+
Tout=tf.float32
51+
)
52+
53+
return optimized_w
54+
55+
class HRPDiffOptPortfolio(tf.keras.Model):
56+
def __init__(self, input_dim, n_assets, hidden_dim):
57+
super(HRPDiffOptPortfolio, self).__init__()
58+
self.feature_extractor = tf.keras.Sequential([
59+
tf.keras.layers.Dense(hidden_dim, activation='relu', input_shape=(input_dim,)),
60+
tf.keras.layers.Dense(hidden_dim, activation='relu')
61+
])
62+
self.returns_predictor = tf.keras.layers.Dense(n_assets)
63+
self.hrp_optimizer = HierarchicalRiskParityOptimizer(n_assets)
64+
65+
def call(self, inputs):
66+
features = self.feature_extractor(inputs)
67+
returns = self.returns_predictor(features)
68+
return self.hrp_optimizer([returns])
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import tensorflow as tf
2+
import numpy as np
3+
4+
class MarketEnvironment:
5+
def __init__(self, returns, initial_balance=10000, transaction_cost=0.001):
6+
self.returns = returns
7+
self.initial_balance = initial_balance
8+
self.transaction_cost = transaction_cost
9+
self.reset()
10+
11+
def reset(self):
12+
self.balance = self.initial_balance
13+
self.position = np.zeros(self.returns.shape[1])
14+
self.time = 0
15+
return self._get_state()
16+
17+
def step(self, action):
18+
old_position = self.position
19+
self.position = action
20+
21+
# Apply transaction costs
22+
self.balance -= np.sum(np.abs(self.position - old_position)) * self.balance * self.transaction_cost
23+
24+
# Apply market returns
25+
self.balance *= 1 + np.sum(self.position * self.returns[self.time])
26+
27+
self.time += 1
28+
done = self.time >= len(self.returns)
29+
30+
return self._get_state(), self._get_reward(), done
31+
32+
def _get_state(self):
33+
return np.concatenate([
34+
self.position,
35+
[self.balance],
36+
self.returns[self.time] if self.time < len(self.returns) else np.zeros_like(self.returns[0])
37+
])
38+
39+
def _get_reward(self):
40+
return np.log(self.balance / self.initial_balance)
41+
42+
class Actor(tf.keras.Model):
43+
def __init__(self, state_dim, action_dim):
44+
super(Actor, self).__init__()
45+
self.model = tf.keras.Sequential([
46+
tf.keras.layers.Dense(64, activation='relu', input_shape=(state_dim,)),
47+
tf.keras.layers.Dense(64, activation='relu'),
48+
tf.keras.layers.Dense(action_dim, activation='softmax')
49+
])
50+
51+
def call(self, state):
52+
return self.model(state)
53+
54+
class Critic(tf.keras.Model):
55+
def __init__(self, state_dim):
56+
super(Critic, self).__init__()
57+
self.model = tf.keras.Sequential([
58+
tf.keras.layers.Dense(64, activation='relu', input_shape=(state_dim,)),
59+
tf.keras.layers.Dense(64, activation='relu'),
60+
tf.keras.layers.Dense(1)
61+
])
62+
63+
def call(self, state):
64+
return self.model(state)
65+
66+
class RLDynamicAllocation(tf.keras.Model):
67+
def __init__(self, state_dim, action_dim, lr_actor=0.0001, lr_critic=0.001):
68+
super(RLDynamicAllocation, self).__init__()
69+
self.actor = Actor(state_dim, action_dim)
70+
self.critic = Critic(state_dim)
71+
self.actor_optimizer = tf.keras.optimizers.Adam(lr_actor)
72+
self.critic_optimizer = tf.keras.optimizers.Adam(lr_critic)
73+
74+
def train(self, env, episodes=1000):
75+
for episode in range(episodes):
76+
state = env.reset()
77+
done = False
78+
while not done:
79+
with tf.GradientTape() as tape_actor, tf.GradientTape() as tape_critic:
80+
action_probs = self.actor(tf.convert_to_tensor([state], dtype=tf.float32))
81+
action = tf.random.categorical(tf.math.log(action_probs), 1)[0, 0]
82+
action_onehot = tf.one_hot(action, env.action_space.n)
83+
84+
next_state, reward, done = env.step(action_onehot.numpy())
85+
86+
critic_value = self.critic(tf.convert_to_tensor([state], dtype=tf.float32))
87+
next_critic_value = self.critic(tf.convert_to_tensor([next_state], dtype=tf.float32))
88+
89+
advantage = reward + 0.99 * next_critic_value * (1 - done) - critic_value
90+
actor_loss = -tf.math.log(action_probs[0, action]) * advantage
91+
critic_loss = advantage ** 2
92+
93+
actor_grads = tape_actor.gradient(actor_loss, self.actor.trainable_variables)
94+
critic_grads = tape_critic.gradient(critic_loss, self.critic.trainable_variables)
95+
96+
self.actor_optimizer.apply_gradients(zip(actor_grads, self.actor.trainable_variables))
97+
self.critic_optimizer.apply_gradients(zip(critic_grads, self.critic.trainable_variables))
98+
99+
state = next_state
100+
101+
def get_action(self, state):
102+
action_probs = self.actor(tf.convert_to_tensor([state], dtype=tf.float32))
103+
return action_probs.numpy()[0]

0 commit comments

Comments
 (0)