-
Notifications
You must be signed in to change notification settings - Fork 2
/
fei_capital_allocation.py
286 lines (231 loc) · 11.6 KB
/
fei_capital_allocation.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
"""# User-circulating FEI Capital Allocation Model (CAM) Module
The Capital Allocation Model describes the aggregate movements of user-circulating FEI within the Fei Protocol ecosystem,
between any Deposit or location where users could choose to allocate their FEI.
These locations include:
- FEI Savings Deposit
- Competing yield opportunities (currently a generic money market in the model)
- Liquidity pools (including liquidity provision and FEI released into the user-circulating FEI supply from volatile asset liquidity pool imbalances)
- Simply holding FEI
These capital movements are computed based on a yield and risk weighted target allocation for each location.
At each timestep of the simulation, the actual allocation is rebalanced towards the target allocation, with some degree of uncertainty and delay.
Additionally, in a next step to further formulate FEI demand, the model will describe the minting and redemption of FEI to feed into the capital allocation.
PCV and protocol-owned FEI movements are independent of the Capital Allocation Model and managed directly via governance-implemented protocol policies.
"""
from typing import Dict, List
from model.system_parameters import Parameters
import model.parts.liquidity_pools as liquidity_pools
from scipy.stats import dirichlet
import numpy as np
import pprint
import networkx as nx
import logging
import copy
pp = pprint.PrettyPrinter(indent=4)
def policy_fei_capital_allocation_exogenous_weight_update(
params: Parameters, substep, state_history, previous_state
):
"""## User-circulating FEI Capital Allocation Exogenous Weight Computation Policy
Exogenous, stochastic Dirichlet distribution driven Capital Allocation policy.
"""
# Parameters
dt = params["dt"]
alpha = params["capital_allocation_exogenous_concentration"]
rebalance_duration = params["capital_allocation_rebalance_duration"]
fei_deposit_variables = params["capital_allocation_fei_deposit_variables"]
# State Variables
timestep = previous_state["timestep"]
# Calculate current weights
fei_deposits = [previous_state[key] for key in fei_deposit_variables]
current_deposit_balances = [deposit.balance for deposit in fei_deposits]
total_fei = sum(current_deposit_balances)
current_weights = [balance / total_fei for balance in current_deposit_balances]
# Calculate target weights: stochastic, exogenous weights
# https://en.wikipedia.org/wiki/Dirichlet_distribution
# TODO Take `random_state`` from simulation seed
perturbation = dirichlet.rvs(alpha, size=1, random_state=timestep)[0]
rebalance_rate = np.sqrt(dt / rebalance_duration)
target_weights = rebalance_rate * perturbation + np.array(current_weights)
normalised_target_weights = target_weights / target_weights.sum()
return {
"capital_allocation_target_weights": normalised_target_weights,
}
def policy_fei_capital_allocation_endogenous_weight_update(
params: Parameters, substep, state_history, previous_state
):
"""## User-circulating FEI Capital Allocation Endogenous Weight Computation Policy
Endogenous yield and risk weighted target Capital Allocation policy.
"""
# Parameters
fei_deposit_variables = params["capital_allocation_fei_deposit_variables"]
moving_average_window = params["capital_allocation_yield_rate_moving_average_window"]
# State Variables
timestep = previous_state["timestep"]
volatile_asset_risk_metric = previous_state["volatile_asset_risk_metric"]
# Calculate moving average of yield vector
yield_history_map: Dict[str, List] = {
key: [state[-1][key].yield_rate for state in state_history[-moving_average_window:timestep]]
for key in fei_deposit_variables
}
yield_history: List[List] = yield_history_map.values()
yield_map = {
key: max(sum(yield_history) / moving_average_window, 1e-18)
or previous_state[key].yield_rate
for key, yield_history in yield_history_map.items()
}
yield_vector = np.array(list(yield_map.values()))
# Calculate yield volatility risk
yield_std = np.array([np.std(x) for x in yield_history])
yield_mean = np.array([np.mean(x) for x in yield_history])
yield_risk = yield_std / (yield_mean + 1e-18)
# Calculate volatile asset risk
volatile_asset_risk = {key: 0 for key in fei_deposit_variables}
volatile_asset_risk_override = {
"fei_liquidity_pool_user_deposit": volatile_asset_risk_metric,
"fei_money_market_user_deposit": volatile_asset_risk_metric,
}
volatile_asset_risk_intersection = (
volatile_asset_risk.keys() & volatile_asset_risk_override.keys()
)
volatile_asset_risk.update(
{
update_key: volatile_asset_risk_override[update_key]
for update_key in volatile_asset_risk_intersection
}
)
volatile_asset_risk = np.array(list(volatile_asset_risk.values()))
# Calculate risk vector
risk_vector = 1 + volatile_asset_risk + yield_risk
assert yield_vector.sum() > 0, "zero or negative yield vector sum"
assert risk_vector.sum() > 0, "zero or negative risk vector sum"
# Calculate target weights: weight = yield / (1 + risk)
target_weights = yield_vector / risk_vector
assert target_weights.sum() >= 0, "zero or negative weights sum"
normalised_target_weights = target_weights / target_weights.sum()
return {
"capital_allocation_target_weights": normalised_target_weights,
}
def array_sum_threshold_check(array, total, threshold):
"""## Array Sum Threshold Check
A function to check that the sum of a Numpy `array` is less than some `total` value within some `threshold`
"""
return np.abs(sum(array) - total) < threshold
def policy_fei_capital_allocation_rebalancing(
params: Parameters, substep, state_history, previous_state
):
"""## User-circulating FEI Capital Allocation Rebalancing Policy
A Policy that takes the target Capital Allocation weights calculated in `policy_fei_capital_allocation_endogenous_weight_update(...)`,
calculates the current Capital Allocation weights, and performs the necessary rebalancing operations to try meet the target.
"""
# Parameters
dt = params["dt"]
rebalance_duration = params["capital_allocation_rebalance_duration"]
fei_deposit_variables = params["capital_allocation_fei_deposit_variables"]
# State Variables
target_weights: np.ndarray = previous_state["capital_allocation_target_weights"]
fei_price = previous_state["fei_price"]
# Calculate current weights
fei_deposits = [copy.deepcopy(previous_state[key]) for key in fei_deposit_variables]
current_deposit_balances = np.array([deposit.balance for deposit in fei_deposits])
total_fei = sum(current_deposit_balances)
current_weights = np.array([balance / total_fei for balance in current_deposit_balances])
assert array_sum_threshold_check(current_deposit_balances, total_fei, 1e-3), "Summation error"
assert array_sum_threshold_check(current_weights, 1, 1e-3), "Percentage calculation error"
assert array_sum_threshold_check(current_weights, 1, 1e-3), "Percentage calculation error"
# Calculate deltas for rebalancing
rebalance_rate = np.sqrt(dt / rebalance_duration)
(
rebalance_matrix,
total_fei_deposit_balance_change,
) = compute_capital_allocation_rebalance_matrix(
target_weights, current_weights, total_fei, rebalance_rate
)
for (row, column), value in filter(lambda x: x != 0, np.ndenumerate(rebalance_matrix)):
# Perform balance transfer
from_index = column if value > 0 else row
to_index = row if value > 0 else column
transfer_amount = min(abs(value), fei_deposits[from_index].balance)
fei_deposits[from_index].transfer(
to=fei_deposits[to_index],
amount=transfer_amount,
from_asset_price=fei_price,
to_asset_price=fei_price,
)
new_capital_allocation = [deposit.balance for deposit in fei_deposits]
# Check constraints
rebalance_remainder = (
current_deposit_balances + total_fei_deposit_balance_change
) - new_capital_allocation
rebalance_remainder_tolerance = 0.001 # % of deposit balance
rebalance_remainder[np.isclose(rebalance_remainder, 0)] = 0
rebalance_remainder_pct = rebalance_remainder / (current_deposit_balances + 1e-9)
if np.any(rebalance_remainder_pct > rebalance_remainder_tolerance):
log_rebalance_remainder = {
deposit.key: rebalance_remainder[index] for index, deposit in enumerate(fei_deposits)
}
logging.debug(
f"Capital allocation rebalancing: movement of {log_rebalance_remainder} FEI unallocated"
)
assert array_sum_threshold_check(new_capital_allocation, total_fei, 1e-3), "Summation error"
return {
"capital_allocation_rebalance_matrix": rebalance_matrix,
"capital_allocation_rebalance_remainder": rebalance_remainder,
# FEI User Deposit updates
**{key: fei_deposits[index] for index, key in enumerate(fei_deposit_variables)},
**(
liquidity_pools.update_fei_liquidity(
previous_state,
dict(zip(fei_deposit_variables, fei_deposits))["fei_liquidity_pool_user_deposit"],
)
if "fei_liquidity_pool_user_deposit" in fei_deposit_variables
else {}
),
}
def compute_capital_allocation_rebalance_matrix(
target_fei_allocation,
current_fei_allocation,
total_fei,
rebalance_rate=1,
):
"""## Compute Capital Allocation Rebalance Matrix
A function that computes the User Deposit rebalancing operations necessary to
meet the target Capital Allocation.
"""
# Calculate delta matrix - amounts to rebalance and to disaggregate
allocation_pct_change = target_fei_allocation - current_fei_allocation
total_fei_deposit_balance_change = rebalance_rate * allocation_pct_change * total_fei
number_of_deposits = len(total_fei_deposit_balance_change)
deposit_incidence_matrix = generate_constrained_incidence_matrix(number_of_deposits)
total_balance_changes = np.append(total_fei_deposit_balance_change, np.array(0))
# Solve Ax = b st 1Tx == 0 (conservation constraint)
deltas = np.linalg.pinv(deposit_incidence_matrix) @ total_balance_changes
assert np.allclose(
np.dot(deposit_incidence_matrix, deltas),
total_balance_changes,
atol=1e-3,
), "Linear algebra solution is above imprecision tolerance"
rebalance_matrix = populate_delta_triangular_matrix(deltas, number_of_deposits)
return rebalance_matrix, total_fei_deposit_balance_change
def populate_delta_triangular_matrix(d, w_size):
"""## Populate Delta Triangular Matrix
A function that populates a lower triangular matrix, sometimes referred to as a `triu` function.
"""
D = np.zeros((w_size, w_size))
k = 0
for i in range(len(D)):
for j in range(len(D)):
if i < j:
D[i][j] = d[k]
k += 1
return D
def generate_constrained_incidence_matrix(n_deposits):
"""## Generate Constrained Incidence Matrix
A function that calculates the incidence matrix for the graph of User Deposits,
in order to be able to calculate the transactions needed to rebalance towards the target Capital Allocation.
"""
G = nx.complete_graph(n_deposits)
A = (nx.incidence_matrix(G, oriented=True).toarray() * -1).astype(int)
# NOTE Fixes NetworkX rendition of 2-deposit adjacency matrix generation
if A.shape[1] == 1:
A = np.hstack([A, np.zeros((2, 1))])
constrained_incidence_matrix = np.vstack([A, np.ones((1, A.shape[1]))])
return constrained_incidence_matrix