Skip to content

Commit a73f157

Browse files
authored
Redo strategy allocation brownie helper code. (#1202)
New vault value checker Strategist runlogs
1 parent 43dbccc commit a73f157

File tree

4 files changed

+498
-169
lines changed

4 files changed

+498
-169
lines changed

brownie/allocations.py

Lines changed: 122 additions & 167 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
import world
22
import pandas as pd
33
import brownie
4+
import re
45

56
NAME_TO_STRAT = {
67
"Convex": world.convex_strat,
78
"AAVE": world.aave_strat,
89
"COMP": world.comp_strat,
10+
"MORPHO_COMP": world.morpho_comp_strat,
11+
"OUSD_META": world.ousd_meta_strat,
912
}
1013

1114
NAME_TO_TOKEN = {
@@ -14,192 +17,144 @@
1417
"USDT": world.usdt,
1518
}
1619

20+
CORE_STABLECOINS = {
21+
"DAI": world.dai,
22+
"USDC": world.usdc,
23+
"USDT": world.usdt,
24+
}
25+
26+
SNAPSHOT_NAMES = {
27+
"Aave DAI": ["AAVE", "DAI"],
28+
"Aave USDC": ["AAVE", "USDC"],
29+
"Aave USDT": ["AAVE", "USDT"],
30+
"Compound DAI": ["COMP", "DAI"],
31+
"Compound USDC": ["COMP", "USDC"],
32+
"Compound USDT": ["COMP", "USDT"],
33+
"Morpho Compound DAI": ["MORPHO_COMP", "DAI"],
34+
"Morpho Compound USDC": ["MORPHO_COMP", "USDC"],
35+
"Morpho Compound USDT": ["MORPHO_COMP", "USDT"],
36+
"Convex DAI/USDC/USDT": ["CONVEX", "*"],
37+
"Convex OUSD/3Crv": ["OUSD_META", "*"],
38+
}
39+
1740

1841
def load_from_blockchain():
1942
base = pd.DataFrame.from_records(
2043
[
2144
["AAVE", "DAI", int(world.aave_strat.checkBalance(world.DAI) / 1e18)],
22-
['AAVE','USDC', int(world.aave_strat.checkBalance(world.USDC)/1e6)],
45+
["AAVE", "USDC", int(world.aave_strat.checkBalance(world.USDC) / 1e6)],
2346
["AAVE", "USDT", int(world.aave_strat.checkBalance(world.USDT) / 1e6)],
2447
["COMP", "DAI", int(world.comp_strat.checkBalance(world.DAI) / 1e18)],
2548
["COMP", "USDC", int(world.comp_strat.checkBalance(world.USDC) / 1e6)],
2649
["COMP", "USDT", int(world.comp_strat.checkBalance(world.USDT) / 1e6)],
2750
["MORPHO_COMP", "DAI", int(world.morpho_comp_strat.checkBalance(world.DAI) / 1e18)],
2851
["MORPHO_COMP", "USDC", int(world.morpho_comp_strat.checkBalance(world.USDC) / 1e6)],
2952
["MORPHO_COMP", "USDT", int(world.morpho_comp_strat.checkBalance(world.USDT) / 1e6)],
30-
["Convex", "*", int(world.convex_strat.checkBalance(world.DAI) * 3 / 1e18)],
31-
["OUSD_META", "*", int(world.ousd_metastrat.checkBalance(world.DAI) * 3 / 2 / 1e18)],
53+
["CONVEX", "*", int(world.convex_strat.checkBalance(world.DAI) * 3 / 1e18)],
54+
["OUSD_META", "*", int(world.ousd_meta_strat.checkBalance(world.DAI) * 3 / 2 / 1e18)],
3255
],
3356
columns=["strategy", "token", "current_dollars"],
3457
)
3558
base["current_allocation"] = base["current_dollars"] / base["current_dollars"].sum()
3659
return base
3760

3861

39-
def add_voting_results(base, vote_results):
40-
vote_allocations = pd.DataFrame.from_records(
41-
vote_results, columns=["strategy", "token", "vote_allocation"]
42-
)
43-
vote_allocations["vote_allocation"] /= 100
44-
allocations = base.merge(
45-
vote_allocations, how="outer", on=["strategy", "token"]
46-
).fillna(0)
47-
allocations = allocations.sort_values(["token", "strategy"])
48-
allocations["vote_dollars"] = (
49-
allocations["vote_allocation"] * allocations["current_dollars"].sum()
50-
).astype("int64")
51-
allocations["vote_change"] = (
52-
allocations["vote_dollars"] - allocations["current_dollars"]
53-
)
54-
return allocations
55-
56-
57-
def add_needed_changes(allocations):
58-
df = allocations
59-
MOVE_THRESHOLD = df.current_dollars.sum() * 0.005
60-
df["remaining_change"] = df[df["vote_change"].abs() > MOVE_THRESHOLD]["vote_change"]
61-
df["remaining_change"] = (df["remaining_change"].fillna(0) / 100000).astype(
62-
"int64"
63-
) * 100000
64-
df["actual_change"] = 0
62+
def reallocate(from_strat, to_strat, funds):
63+
"""
64+
Execute and return a transaction reallocating funds from one strat to another
65+
"""
66+
amounts = []
67+
coins = []
68+
for [dollars, coin] in funds:
69+
amounts.append(int(dollars * 10 ** coin.decimals()))
70+
coins.append(coin)
71+
return world.vault_admin.reallocate(from_strat, to_strat, coins, amounts, {"from": world.STRATEGIST})
72+
73+
74+
def allocation_exposure(allocation):
75+
"""
76+
Shows how exposed we would be to a stablecoin peg loss.
77+
78+
Consevitivly assumes that:
79+
- Any Curve pool would go 100% to the peg lost coin
80+
- DAI would follow a USDC peg loss.
81+
82+
Reality may not be quite so bad.
83+
"""
84+
exposure_masks = {
85+
"DAI": (allocation["token"] == "DAI") | (allocation["token"] == "*"),
86+
"USDC": (allocation["token"] == "USDC") | (allocation["token"] == "DAI") | (allocation["token"] == "*"),
87+
"USDT": (allocation["token"] == "USDT") | (allocation["token"] == "*"),
88+
}
89+
total = allocation["current_dollars"].sum()
90+
print("Maximum exposure: ")
91+
for coin, mask in exposure_masks.items():
92+
coin_exposure = allocation[mask]["current_dollars"].sum() / total
93+
print(" {:<6} {:,.2%}".format(coin, coin_exposure))
94+
95+
96+
def lookup_strategy(address):
97+
for name, contract in NAME_TO_STRAT.items():
98+
if contract.address.lower() == address.lower():
99+
return [name, contract]
100+
101+
102+
def show_default_strategies():
103+
print("Default Strategies:")
104+
for coin_name, coin in CORE_STABLECOINS.items():
105+
default_strat_address = world.vault_core.assetDefaultStrategies(coin)
106+
name, strat = lookup_strategy(default_strat_address)
107+
raw_funds = strat.checkBalance(coin)
108+
decimals = coin.decimals()
109+
funds = int(raw_funds / (10**decimals))
110+
print("{:>6} defaults to {} with {:,}".format(coin_name, name, funds))
111+
112+
113+
def with_target_allocations(allocation, votes):
114+
df = allocation.copy()
115+
df["target_allocation"] = float(0.0)
116+
if isinstance(votes, pd.DataFrame):
117+
df["target_allocation"] = votes["target_allocation"]
118+
else:
119+
for line in votes.splitlines():
120+
m = re.search(r"[ \t]*(.+)[ \t]([0-9.]+)", line)
121+
if not m:
122+
continue
123+
strat_name = m.group(1).strip()
124+
strat_alloc = float(m.group(2)) / 100.0
125+
if strat_name in SNAPSHOT_NAMES:
126+
[internal_name, internal_coin] = SNAPSHOT_NAMES[strat_name]
127+
mask = (df.strategy == internal_name) & (df.token == internal_coin)
128+
df.loc[mask, "target_allocation"] += strat_alloc
129+
elif strat_name == "Existing Allocation":
130+
pass
131+
df["target_allocation"] += df["current_allocation"] * strat_alloc
132+
else:
133+
raise Exception('Could not look up strategy name "%s"' % strat_name)
134+
135+
if df["target_allocation"].sum() > 1.02:
136+
print(df)
137+
print(df["target_allocation"].sum())
138+
raise Exception("Target allocations total too high")
139+
if df["target_allocation"].sum() < 0.98:
140+
print(df)
141+
print(df["target_allocation"].sum())
142+
raise Exception("Target allocations total too low")
143+
144+
df["target_dollars"] = (
145+
df["current_dollars"].sum() * df["target_allocation"] / df["target_allocation"].sum()
146+
).astype(int)
147+
df["delta_dollars"] = df["target_dollars"] - df["current_dollars"]
65148
return df
66149

67150

68-
def plan_moves(allocations):
69-
possible_strat_moves = [
70-
["AAVE", "COMP"],
71-
["COMP", "AAVE"],
72-
["Convex", "COMP"],
73-
["Convex", "AAVE"],
74-
["AAVE", "Convex"],
75-
["COMP", "Convex"],
76-
]
77-
tokens = ["DAI", "USDC", "USDT"]
78-
79-
moves = []
80-
81-
df = allocations
82-
for strat_from, strat_to in possible_strat_moves:
83-
for token in tokens:
84-
token_match = (df["token"] == token) | (df["token"] == "*")
85-
from_filter = token_match & (df["strategy"] == strat_from)
86-
to_filter = token_match & (df["strategy"] == strat_to)
87-
from_row = df.loc[from_filter]
88-
to_row = df.loc[to_filter]
89-
from_change = from_row.remaining_change.values[0]
90-
to_change = to_row.remaining_change.values[0]
91-
from_strategy = from_row.strategy.values[0]
92-
to_strategy = to_row.strategy.values[0]
93-
94-
if from_change < 0 and to_change > 0:
95-
move_change = min(to_change, -1 * from_change)
96-
df.loc[from_filter, "remaining_change"] += move_change
97-
df.loc[to_filter, "remaining_change"] -= move_change
98-
df.loc[from_filter, "actual_change"] -= move_change
99-
df.loc[to_filter, "actual_change"] += move_change
100-
moves.append([from_strategy, to_strategy, token, move_change])
101-
102-
moves = pd.DataFrame.from_records(moves, columns=["from", "to", "token", "amount"])
103-
return df, moves
104-
105-
106-
def print_headline(text):
107-
print("------------")
108-
print(text)
109-
print("------------")
110-
111-
112-
def generate_transactions(moves):
113-
move_txs = []
114-
notes = []
115-
with world.TemporaryFork():
116-
before_total = world.vault_core.totalValue()
117-
118-
for from_to, inner_moves in moves.groupby(["from", "to"]):
119-
from_strategy = NAME_TO_STRAT[from_to[0]]
120-
to_strategy = NAME_TO_STRAT[from_to[1]]
121-
tokens = [NAME_TO_TOKEN[x] for x in inner_moves["token"]]
122-
dollars = [x for x in inner_moves["amount"]]
123-
raw_amounts = [
124-
10 ** token.decimals() * int(amount)
125-
for token, amount in zip(tokens, dollars)
126-
]
127-
notes.append(
128-
"- From %s to %s move %s"
129-
% (
130-
from_to[0],
131-
from_to[1],
132-
", ".join(
133-
[
134-
"%s million %s" % (d / 1000000, t)
135-
for t, d in zip(inner_moves["token"], dollars)
136-
]
137-
),
138-
)
139-
)
140-
141-
tx = world.vault_admin.reallocate(
142-
from_strategy,
143-
to_strategy,
144-
tokens,
145-
raw_amounts,
146-
{"from": world.strategist},
147-
)
148-
move_txs.append(tx)
149-
150-
after_total = world.vault_core.totalValue()
151-
vault_loss_raw = before_total - after_total
152-
vault_loss_dollars = int(vault_loss_raw / 1e18)
153-
154-
print_headline("After Move")
155-
after = load_from_blockchain()
156-
after = after.rename(
157-
{
158-
"current_allocation": "percent",
159-
"current_dollars": "dollars",
160-
}
161-
)
162-
print(
163-
after.to_string(
164-
formatters={
165-
"percent": "{:,.2%}".format,
166-
"dollars": "{:,}".format,
167-
}
168-
)
169-
)
170-
print("Expected loss from move: ${:,}".format(vault_loss_dollars))
171-
return move_txs, notes, vault_loss_raw
172-
173-
174-
def wrap_in_loss_prevention(moves, vault_loss_raw):
175-
max_loss = int(vault_loss_raw) + int(abs(vault_loss_raw) * 0.1) + 100 * 1e18
176-
new_moves = []
177-
with world.TemporaryFork():
178-
new_moves.append(
179-
world.vault_value_checker.takeSnapshot({"from": world.STRATEGIST})
180-
)
181-
new_moves = new_moves + moves
182-
new_moves.append(
183-
world.vault_value_checker.checkLoss(max_loss, {"from": world.STRATEGIST})
184-
)
185-
print(
186-
"Expected loss: ${:,} Allowed loss from move: ${:,}".format(
187-
int(vault_loss_raw // 1e18), int(max_loss // 1e18)
188-
)
189-
)
190-
return new_moves
191-
192-
193-
def transactions_for_reallocation(votes):
194-
base = load_from_blockchain()
195-
allocations = add_needed_changes(add_voting_results(base, votes))
196-
allocations, moves = plan_moves(allocations)
197-
print_headline("Current, Voting, and planned allocations")
198-
print(allocations)
199-
txs, notes, vault_loss_raw = generate_transactions(moves)
200-
print_headline("Plan")
201-
print("Planned strategist moves:")
202-
print("\n".join(notes))
203-
txs = wrap_in_loss_prevention(txs, vault_loss_raw)
204-
205-
return txs
151+
def pretty_allocations(allocation, close_enough=50_000):
152+
df = allocation.copy()
153+
df["s"] = ""
154+
df.loc[df["delta_dollars"].abs() < close_enough, "s"] = "✔︎"
155+
df["current_allocation"] = df["current_allocation"].apply("{:.2%}".format)
156+
df["target_allocation"] = df["target_allocation"].apply("{:.2%}".format)
157+
df["current_dollars"] = df["current_dollars"].apply("{:,}".format)
158+
df["target_dollars"] = df["target_dollars"].apply("{:,}".format)
159+
df["delta_dollars"] = df["delta_dollars"].apply("{:,}".format)
160+
return df.sort_values("token")

0 commit comments

Comments
 (0)