Master vault strategy deposits_disabledbeta
Multi-vault allocation strategy on Arbitrum
Source code
The source code of the Master vault strategy strategy
"""Vault of vaults strategy.
Based on `05-tweaked-basket-construction.ipynb` notebook.
Tweaked basket construction criteria to bump the yield a bit.
This is a multi-vault allocation strategy that:
- Selects from a universe of DeFi vaults on Arbitrum
- Rebalances weekly based on rolling returns
- Caps individual position sizes and concentration
- Uses TVL-based filtering for vault inclusion
Backtest results (2025-01-06 to 2025-12-29)
=============================================
Last backtest run: 2026-01-15
================================ ========= ======
Metric Strategy ETH
================================ ========= ======
Start period 2025-01-06 2025-01-06
End period 2025-12-29 2025-12-29
Risk-free rate 0.0% 0.0%
Time in market 15.0% 98.0%
Cumulative return 14.81% -10.1%
CAGR﹪ 15.17% -10.32%
Sharpe 3.95 0.23
Probabilistic Sharpe ratio 100.0% 58.86%
Smart Sharpe 3.79 0.22
Sortino 179.75 0.34
Smart Sortino 172.22 0.33
Sortino/√2 127.1 0.24
Smart Sortino/√2 121.77 0.23
Omega 179.02 179.02
Max drawdown -0.08% -57.61%
Longest DD days 7 180
Volatility (ann.) 3.58% 75.07%
Calmar 194.58 -0.18
================================ ========= ======
"""
#
# Imports
#
import datetime
import logging
import pandas as pd
from eth_defi.token import USDC_NATIVE_TOKEN
from eth_defi.vault.vaultdb import DEFAULT_RAW_PRICE_DATABASE
from plotly.graph_objects import Figure
from tradingstrategy.chain import ChainId
from tradingstrategy.timebucket import TimeBucket
from tradingstrategy.utils.token_filter import filter_for_selected_pairs
from tradeexecutor.analysis.vault import display_vaults
from tradeexecutor.state.identifier import TradingPairIdentifier
from tradeexecutor.state.trade import TradeExecution
from tradeexecutor.state.types import USDollarAmount
from tradeexecutor.strategy.alpha_model import AlphaModel
from tradeexecutor.strategy.chart.definition import (ChartInput, ChartKind,
ChartRegistry)
from tradeexecutor.strategy.chart.standard.alpha_model import \
alpha_model_diagnostics
from tradeexecutor.strategy.chart.standard.equity_curve import (
equity_curve, equity_curve_with_drawdown)
from tradeexecutor.strategy.chart.standard.interest import (
lending_pool_interest_accrued, vault_statistics)
from tradeexecutor.strategy.chart.standard.performance_metrics import \
performance_metrics
from tradeexecutor.strategy.chart.standard.position import positions_at_end
from tradeexecutor.strategy.chart.standard.profit_breakdown import \
trading_pair_breakdown
from tradeexecutor.strategy.chart.standard.signal import (price_vs_signal,
signal_comparison)
from tradeexecutor.strategy.chart.standard.single_pair import (
trading_pair_positions, trading_pair_price_and_trades)
from tradeexecutor.strategy.chart.standard.thinking import last_messages
from tradeexecutor.strategy.chart.standard.trading_metrics import \
trading_metrics
from tradeexecutor.strategy.chart.standard.trading_universe import (
available_trading_pairs, inclusion_criteria_check)
from tradeexecutor.strategy.chart.standard.vault import all_vault_positions
from tradeexecutor.strategy.chart.standard.vault import \
all_vaults_share_price_and_tvl as _all_vaults_share_price_and_tvl
from tradeexecutor.strategy.chart.standard.vault import vault_position_timeline
from tradeexecutor.strategy.chart.standard.volatility import \
volatility_benchmark
from tradeexecutor.strategy.chart.standard.weight import (
equity_curve_by_asset, volatile_and_non_volatile_percent,
volatile_weights_by_percent, weight_allocation_statistics)
from tradeexecutor.strategy.cycle import CycleDuration
from tradeexecutor.strategy.default_routing_options import TradeRouting
from tradeexecutor.strategy.execution_context import (ExecutionContext,
ExecutionMode)
from tradeexecutor.strategy.pandas_trader.indicator import (
IndicatorDependencyResolver, IndicatorSource)
from tradeexecutor.strategy.pandas_trader.indicator_decorator import \
IndicatorRegistry
from tradeexecutor.strategy.pandas_trader.strategy_input import StrategyInput
from tradeexecutor.strategy.pandas_trader.trading_universe_input import \
CreateTradingUniverseInput
from tradeexecutor.strategy.parameters import StrategyParameters
from tradeexecutor.strategy.tag import StrategyTag
from tradeexecutor.strategy.trading_strategy_universe import (
TradingStrategyUniverse, load_partial_data, load_vault_universe_with_metadata)
from tradeexecutor.strategy.tvl_size_risk import USDTVLSizeRiskModel
from tradeexecutor.strategy.universe_model import UniverseOptions
from tradeexecutor.strategy.weighting import weight_passthrouh
from tradeexecutor.utils.dedent import dedent_any
logger = logging.getLogger(__name__)
#
# Trading universe constants
#
trading_strategy_engine_version = "0.5"
CHAIN_ID = ChainId.arbitrum
EXCHANGES = ("uniswap-v2", "uniswap-v3")
SUPPORTING_PAIRS = [
(ChainId.arbitrum, "uniswap-v3", "WETH", "USDC", 0.0005),
]
LENDING_RESERVES = None
PREFERRED_STABLECOIN = USDC_NATIVE_TOKEN[CHAIN_ID].lower()
VAULTS = [
(ChainId.arbitrum, "0x58bfc95a864e18e8f3041d2fcd3418f48393fe6a"), # Plutus Hedge Token
(ChainId.arbitrum, "0x959f3807f0aa7921e18c78b00b2819ba91e52fef"), # gmUSDC
(ChainId.arbitrum, "0xd3443ee1e91af28e5fb858fbd0d72a63ba8046e0"), # gTrade (Gains) USDC
(ChainId.arbitrum, "0x75288264fdfea8ce68e6d852696ab1ce2f3e5004"), # Hype++
(ChainId.arbitrum, "0x4b6f1c9e5d470b97181786b26da0d0945a7cf027"), # Hypertrim USDC
(ChainId.arbitrum, "0x0b2b2b2076d95dda7817e785989fe353fe955ef9"), # Staked USDai
(ChainId.arbitrum, "0x64ca76e2525fc6ab2179300c15e343d73e42f958"), # Clearstar high yielsd USDC
(ChainId.arbitrum, "0x7e97fa6893871a2751b5fe961978dccb2c201e65"), # Gauntlet
(ChainId.arbitrum, "0x1a996cb54bb95462040408c06122d45d6cdb6096"), # Fluid
(ChainId.arbitrum, "0xa91267a25939b2b0f046013fbf9597008f7f014b"), # IPOR USDC Arbirum optimise
(ChainId.arbitrum, "0x05d28a86e057364f6ad1a88944297e58fc6160b3"), # Euler Arbitrum Yield USDC
(ChainId.arbitrum, "0x20d419a8e12c45f88fda7c5760bb6923cee27f98"), # Ostium liquidity provider
(ChainId.arbitrum, "0x20d419a8e12c45f88fda7c5760bb6923cee27f98"), # Ostium liquidity provider
# Some smaller entries to mix in
(ChainId.arbitrum, "0xc8248953429d707c6a2815653eca89846ffaa63b"), # Curve LLAMMA asdCRV / crvUSD
(ChainId.arbitrum, "0xf63b7f49b4f5dc5d0e7e583cfd79dc64e646320c"), # Auto finance Tokemak ARB/USDC
(ChainId.arbitrum, "0xeeaf2ccb73a01deb38eca2947d963d64cfde6a32"), # Curve LLAMMA CRV / crvUSD
(ChainId.arbitrum, "0xe5d6eb448ac5a762c1ebe8cd1692b9cd08025176"), # DAMM stablecoin fund
]
BENCHMARK_PAIRS = [
(ChainId.arbitrum, "uniswap-v3", "WETH", "USDC", 0.0005),
]
# Exclude Euro vaults, etc.
ALLOWED_VAULT_DENOMINATION_TOKENS = {"USDC", "USDT", "USDC.e", "crvUSD", "USDai", "USD₮0"}
#
# Strategy parameters
#
class Parameters:
id = "master-vault"
# We trade 1h candle
candle_time_bucket = TimeBucket.d1
cycle_duration = CycleDuration.cycle_7d
chain_id = CHAIN_ID
exchanges = EXCHANGES
#
# Basket size, risk and balancing parametrs.
#
min_asset_universe = 5 # How many assets we need in the asset universe to start running the index
max_assets_in_portfolio = 4 # How many assets our basket can hold once
allocation = 0.95 # Allocate all cash to volatile pairs
individual_rebalance_min_threshold_usd = 500.0 # Don't make buys less than this amount
sell_rebalance_min_threshold = 100.0
sell_threshold = 0.05 # Sell if asset is more than 5% of the portfolio
per_position_cap_of_pool = 0.33 # Never own more than % of the lit liquidity of the trading pool
max_concentration = 0.25 # How large % can one asset be in a portfolio once
min_portfolio_weight = 0.0050 # Close position / do not open if weight is less than 50 BPS
# How long
# Needed to calculate weights
rolling_returns_bars = 32
min_tvl = 50_000 # Minimum TVL in the vault before it can be considered investable
#
#
# Backtesting only
# Limiting factor: Aave v3 on Base starts at the end of DEC 2023
#
backtest_start = datetime.datetime(2025, 1, 1)
backtest_end = datetime.datetime(2026, 1, 1)
initial_cash = 100_000
#
# Live only
#
routing = TradeRouting.default
required_history_period = datetime.timedelta(days=365*3)
slippage_tolerance = 0.0060 # 0.6%
assummed_liquidity_when_data_missings = 10_000
#
# Universe creation
#
def create_trading_universe(
input: CreateTradingUniverseInput,
) -> TradingStrategyUniverse:
"""Create the trading universe.
- Load Trading Strategy full pairs dataset
- Load built-in Coingecko top 1000 dataset
- Get all DEX tokens for a certain Coigecko category
- Load OHCLV data for these pairs
- Load also BTC and ETH price data to be used as a benchmark
"""
execution_context = input.execution_context
client = input.client
timestamp = input.timestamp
parameters = input.parameters or Parameters # Some CLI commands do not support yet passing this
universe_options = input.universe_options
if execution_context.live_trading:
# Live trading, send strategy universe formation details
# to logs
debug_printer = logger.info
else:
# Jupyter notebook inline output
debug_printer = print
chain_id = parameters.chain_id
debug_printer(f"Preparing trading universe on chain {chain_id.get_name()}")
# Pull out our benchmark pairs ids.
# We need to construct pair universe object for the symbolic lookup.
all_pairs_df = client.fetch_pair_universe().to_pandas()
pairs_df = filter_for_selected_pairs(
all_pairs_df,
SUPPORTING_PAIRS,
)
debug_printer(f"We have total {len(all_pairs_df)} pairs in dataset and going to use {len(pairs_df)} pairs for the strategy")
# Load vault universe with metadata from JSON blob
# This provides richer metadata including performance metrics, fees, etc.
vault_universe = load_vault_universe_with_metadata(client, vaults=VAULTS)
vault_universe = vault_universe.limit_to_denomination(ALLOWED_VAULT_DENOMINATION_TOKENS, check_all_vaults_found=True)
debug_printer(f"Loaded {vault_universe.get_vault_count()} vaults from JSON vault database")
# Default vault data bundle path for backtesting
vault_bundled_price_data = DEFAULT_RAW_PRICE_DATABASE
debug_printer(f"Using vault price data for backtesting from {vault_bundled_price_data}")
dataset = load_partial_data(
client=client,
time_bucket=parameters.candle_time_bucket,
pairs=pairs_df,
execution_context=execution_context,
universe_options=universe_options,
liquidity=True,
liquidity_time_bucket=TimeBucket.d1,
lending_reserves=LENDING_RESERVES,
vaults=vault_universe,
vault_bundled_price_data=vault_bundled_price_data if not execution_context.live_trading else None,
check_all_vaults_found=True,
)
debug_printer("Creating strategy universe with price feeds and vaults")
strategy_universe = TradingStrategyUniverse.create_from_dataset(
dataset,
reserve_asset=PREFERRED_STABLECOIN,
forward_fill=True, # We got very gappy data from low liquid DEX coins
forward_fill_until=timestamp,
)
# crvUSD etc. do not have backtesting paths yet
strategy_universe.ignore_routing = True
# Dump our vault data and check for data errors
display_vaults(
vault_universe,
strategy_universe,
execution_mode=execution_context.mode,
printer=debug_printer,
)
return strategy_universe
#
# Strategy logic
#
_cached_start_times: dict[int, pd.Timestamp] = {}
def decide_trades(
input: StrategyInput
) -> list[TradeExecution]:
"""For each strategy tick, generate the list of trades."""
parameters = input.parameters
position_manager = input.get_position_manager()
state = input.state
timestamp = input.timestamp
indicators = input.indicators
strategy_universe = input.strategy_universe
portfolio = position_manager.get_current_portfolio()
equity = portfolio.get_total_equity()
# Live trading automation not yet enabled -
# manually check positions while the strategy is in beta mode
if input.execution_context.mode != ExecutionMode.backtesting:
return []
# Build signals for each pair
alpha_model = AlphaModel(
timestamp,
close_position_weight_epsilon=parameters.min_portfolio_weight, # 10 BPS is our min portfolio weight
)
tvl_included_pair_count = indicators.get_indicator_value(
"tvl_included_pair_count",
)
# Get pairs included in this rebalance cycle.
# This includes pair that have been pre-cleared in inclusion_criteria()
# with volume, volatility and TVL filters
included_pairs = indicators.get_indicator_value(
"inclusion_criteria",
na_conversion=False,
)
if included_pairs is None:
included_pairs = []
# Set signal for each pair
signal_count = 0
for pair_id in included_pairs:
pair = strategy_universe.get_pair_by_id(pair_id)
if not state.is_good_pair(pair):
# Tradeable flag set to False, etc.
continue
pair_signal = indicators.get_indicator_value("signal", pair=pair)
if pair_signal is None:
continue
weight = pair_signal
if weight < 0:
continue
alpha_model.set_signal(
pair,
weight,
)
# Diagnostics reporting
signal_count += 1
# Calculate how much dollar value we want each individual position to be on this strategy cycle,
# based on our total available equity
portfolio = position_manager.get_current_portfolio()
equity = portfolio.get_total_equity()
portfolio_target_value = equity * parameters.allocation
# Select max_assets_in_portfolio assets in which we are going to invest
# Calculate a weight for ecah asset in the portfolio using 1/N method based on the raw signal
alpha_model.select_top_signals(count=parameters.max_assets_in_portfolio)
alpha_model.assign_weights(method=weight_passthrouh)
#
# Normalise weights and cap the positions
#
size_risk_model = USDTVLSizeRiskModel(
pricing_model=input.pricing_model,
per_position_cap=parameters.per_position_cap_of_pool, # This is how much % by all pool TVL we can allocate for a position
missing_tvl_placeholder_usd=0.0, # Placeholder for missing TVL data until we get the data off the chain
)
alpha_model.normalise_weights(
investable_equity=portfolio_target_value,
size_risk_model=size_risk_model,
max_weight=parameters.max_concentration,
)
# Load in old weight for each trading pair signal,
# so we can calculate the adjustment trade size
alpha_model.update_old_weights(
state.portfolio,
ignore_credit=False,
)
alpha_model.calculate_target_positions(position_manager)
# Shift portfolio from current positions to target positions
# determined by the alpha signals (momentum)
rebalance_threshold_usd = parameters.individual_rebalance_min_threshold_usd
assert rebalance_threshold_usd > 0.1, "Safety check tripped - something like wrong with strat code"
trades = alpha_model.generate_rebalance_trades_and_triggers(
position_manager,
min_trade_threshold=rebalance_threshold_usd, # Don't bother with trades under XXXX USD
invidiual_rebalance_min_threshold=parameters.individual_rebalance_min_threshold_usd,
sell_rebalance_min_threshold=parameters.sell_rebalance_min_threshold,
execution_context=input.execution_context,
)
# Add verbal report about decision made/not made,
# so it is much easier to diagnose live trade execution.
# This will be readable in Discord/Telegram logging.
if input.is_visualisation_enabled():
try:
top_signal = next(iter(alpha_model.get_signals_sorted_by_weight()))
if top_signal.normalised_weight == 0:
top_signal = None
except StopIteration:
top_signal = None
rebalance_volume = sum(t.get_value() for t in trades)
report = dedent_any(f"""
Cycle: #{input.cycle}
Rebalanced: {'👍' if alpha_model.is_rebalance_triggered() else '👎'}
Open/about to open positions: {len(state.portfolio.open_positions)}
Max position value change: {alpha_model.max_position_adjust_usd:,.2f} USD
Rebalance threshold: {alpha_model.position_adjust_threshold_usd:,.2f} USD
Trades decided: {len(trades)}
Pairs total: {strategy_universe.data_universe.pairs.get_count()}
Pairs meeting inclusion criteria: {len(included_pairs)}
Pairs meeting TVL inclusion criteria: {tvl_included_pair_count}
Signals created: {signal_count}
Total equity: {portfolio.get_total_equity():,.2f} USD
Cash: {position_manager.get_current_cash():,.2f} USD
Investable equity: {alpha_model.investable_equity:,.2f} USD
Accepted investable equity: {alpha_model.accepted_investable_equity:,.2f} USD
Allocated to signals: {alpha_model.get_allocated_value():,.2f} USD
Discarted allocation because of lack of lit liquidity: {alpha_model.size_risk_discarded_value:,.2f} USD
Rebalance volume: {rebalance_volume:,.2f} USD
""")
if top_signal:
assert top_signal.position_size_risk
report += dedent_any(f"""
Top signal pair: {top_signal.pair.get_ticker()}
Top signal value: {top_signal.signal}
Top signal weight: {top_signal.raw_weight}
Top signal weight (normalised): {top_signal.normalised_weight * 100:.2f} % (got {top_signal.position_size_risk.get_relative_capped_amount() * 100:.2f} % of asked size)
""")
for flag, count in alpha_model.get_flag_diagnostics_data().items():
report += f"Signals with flag {flag.name}: {count}\n"
state.visualisation.add_message(
timestamp,
report,
)
state.visualisation.set_discardable_data("alpha_model", alpha_model)
return trades # Return the list of trades we made in this cycle
#
# Indicators
#
empty_series = pd.Series([], index=pd.DatetimeIndex([]))
indicators = IndicatorRegistry()
@indicators.define()
def rolling_returns(
close: pd.Series,
rolling_returns_bars: int = 60,
) -> pd.Series:
"""Calculate rolling returns over a period"""
windowed = close.rolling(
window=rolling_returns_bars,
min_periods=2,
).max()
series = (close / windowed)
return series
@indicators.define(source=IndicatorSource.tvl)
def tvl(
close: pd.Series,
execution_context: ExecutionContext,
timestamp: pd.Timestamp,
) -> pd.Series:
"""Get TVL series for a pair.
- Because TVL data is 1d and we use 1h everywhere else, we need to forward fill
- Use previous hourly close as the value
"""
if execution_context.live_trading:
# TVL is daily data.
# We need to forward fill until the current hour.
# Use our special ff function.
assert isinstance(timestamp, pd.Timestamp), f"Live trading needs forward-fill end time, we got {timestamp}"
from tradingstrategy.utils.forward_fill import forward_fill
df = pd.DataFrame({"close": close})
df_ff = forward_fill(
df,
Parameters.candle_time_bucket.to_frequency(),
columns=("close",),
forward_fill_until=timestamp,
)
series = df_ff["close"]
return series
else:
return close.resample("1h").ffill()
@indicators.define(dependencies=(tvl,), source=IndicatorSource.dependencies_only_universe)
def tvl_inclusion_criteria(
min_tvl: USDollarAmount,
dependency_resolver: IndicatorDependencyResolver,
) -> pd.Series:
"""The pair must have min XX,XXX USD one-sided TVL to be included.
- If the Uniswap pool does not have enough ETH or USDC deposited, skip the pair as a scam
:return:
Series where each timestamp is a list of pair ids meeting the criteria at that timestamp
"""
series = dependency_resolver.get_indicator_data_pairs_combined(tvl)
mask = series >= min_tvl
# Turn to a series of lists
mask_true_values_only = mask[mask == True]
series = mask_true_values_only.groupby(level='timestamp').apply(lambda x: x.index.get_level_values('pair_id').tolist())
return series
@indicators.define(
source=IndicatorSource.strategy_universe
)
def trading_availability_criteria(
strategy_universe: TradingStrategyUniverse,
) -> pd.Series:
"""Is pair tradeable at each hour.
- The pair has a price candle at that
- Mitigates very corner case issues that TVL/liquidity data is per-day whileas price data is natively per 1h
and the strategy inclusion criteria may include pair too early hour based on TVL only,
leading to a failed attempt to rebalance in a backtest
- Only relevant for backtesting issues if we make an unlucky trade on the starting date
of trading pair listing
:return:
Series with with index (timestamp) and values (list of pair ids trading at that hour)
"""
# Trading pair availability is defined if there is a open candle in the index for it.
# Because candle data is forward filled, we should not have any gaps in the index.
candle_series = strategy_universe.data_universe.candles.df["open"]
pairs_per_timestamp = candle_series.groupby(level='timestamp').apply(lambda x: x.index.get_level_values('pair_id').tolist())
return pairs_per_timestamp
@indicators.define(
dependencies=[
tvl_inclusion_criteria,
trading_availability_criteria
],
source=IndicatorSource.strategy_universe
)
def inclusion_criteria(
strategy_universe: TradingStrategyUniverse,
min_tvl: USDollarAmount,
dependency_resolver: IndicatorDependencyResolver
) -> pd.Series:
"""Pairs meeting all of our inclusion criteria.
- Give the tradeable pair set for each timestamp
:return:
Series where index is timestamp and each cell is a list of pair ids matching our inclusion criteria at that moment
"""
# Filter out benchmark pairs like WETH in the tradeable pair set
benchmark_pair_ids = set(strategy_universe.get_pair_by_human_description(desc).internal_id for desc in SUPPORTING_PAIRS)
tvl_series = dependency_resolver.get_indicator_data(
tvl_inclusion_criteria,
parameters={
"min_tvl": min_tvl,
},
)
trading_availability_series = dependency_resolver.get_indicator_data(trading_availability_criteria)
#
# Process all pair ids as a set and the final inclusion
# criteria is union of all sub-criterias
#
df = pd.DataFrame({
"tvl_pair_ids": tvl_series,
"trading_availability_pair_ids": trading_availability_series,
})
# https://stackoverflow.com/questions/33199193/how-to-fill-dataframe-nan-values-with-empty-list-in-pandas
df = df.fillna("").apply(list)
def _combine_criteria(row):
final_set = set(row["tvl_pair_ids"]) & \
set(row["trading_availability_pair_ids"])
return final_set - benchmark_pair_ids
union_criteria = df.apply(_combine_criteria, axis=1)
# Inclusion criteria data can be spotty at the beginning when there is only 0 or 1 pairs trading,
# so we need to fill gaps to 0
full_index = pd.date_range(
start=union_criteria.index.min(),
end=union_criteria.index.max(),
freq=Parameters.candle_time_bucket.to_frequency(),
)
reindexed = union_criteria.reindex(full_index, fill_value=[])
return reindexed
@indicators.define(dependencies=(tvl_inclusion_criteria,), source=IndicatorSource.dependencies_only_universe)
def tvl_included_pair_count(
min_tvl: USDollarAmount,
dependency_resolver: IndicatorDependencyResolver
) -> pd.Series:
"""Calculate number of pairs in meeting volatility criteria on each timestamp"""
series = dependency_resolver.get_indicator_data(
tvl_inclusion_criteria,
parameters={"min_tvl": min_tvl},
)
series = series.apply(len)
# TVL data can be spotty at the beginning when there is only 0 or 1 pairs trading,
# so we need to fill gaps to 0
full_index = pd.date_range(
start=series.index.min(),
end=series.index.max(),
freq=Parameters.candle_time_bucket.to_frequency(),
)
# Reindex and fill NaN with zeros
reindexed = series.reindex(full_index, fill_value=0)
return reindexed
@indicators.define(dependencies=(inclusion_criteria,), source=IndicatorSource.dependencies_only_universe)
def all_criteria_included_pair_count(
min_tvl: USDollarAmount,
dependency_resolver: IndicatorDependencyResolver
) -> pd.Series:
"""Series where each timestamp is the list of pairs meeting all inclusion criteria.
:return:
Series with pair count for each timestamp
"""
series = dependency_resolver.get_indicator_data(
"inclusion_criteria",
parameters={
"min_tvl": min_tvl,
},
)
return series.apply(len)
@indicators.define(source=IndicatorSource.strategy_universe)
def trading_pair_count(
strategy_universe: TradingStrategyUniverse,
) -> pd.Series:
"""Get number of pairs that trade at each timestamp.
- Pair must have had at least one candle before the timestamp to be included
- Exclude benchmarks pairs we do not trade
:return:
Series with pair count for each timestamp
"""
benchmark_pair_ids = {strategy_universe.get_pair_by_human_description(desc).internal_id for desc in SUPPORTING_PAIRS}
# Get pair_id, timestamp -> timestamp, pair_id index
series = strategy_universe.data_universe.candles.df["open"]
swap_index = series.index.swaplevel(0, 1)
seen_pairs = set()
seen_data = {}
for timestamp, pair_id in swap_index:
if pair_id in benchmark_pair_ids:
continue
seen_pairs.add(pair_id)
seen_data [timestamp] = len(seen_pairs)
series = pd.Series(seen_data.values(), index=list(seen_data.keys()))
return series
@indicators.define(
source=IndicatorSource.dependencies_only_per_pair,
dependencies=[
rolling_returns,
]
)
def signal(
rolling_returns_bars: int,
candle_time_bucket: TimeBucket,
pair: TradingPairIdentifier,
dependency_resolver: IndicatorDependencyResolver
) -> pd.Series:
"""Calculate weighting criteria ("signal") as the past returns of the rolling returns window."""
rolling_returns = dependency_resolver.get_indicator_data(
"rolling_returns",
parameters={
"rolling_returns_bars": rolling_returns_bars,
},
pair=pair,
)
return rolling_returns
def create_indicators(
timestamp: datetime.datetime | None,
parameters: StrategyParameters,
strategy_universe: TradingStrategyUniverse,
execution_context: ExecutionContext,
):
"""Create indicators for the strategy."""
return indicators.create_indicators(
timestamp=timestamp,
parameters=parameters,
strategy_universe=strategy_universe,
execution_context=execution_context,
)
#
# Charts
#
def equity_curve_with_benchmark(input: ChartInput) -> list[Figure]:
"""Add our benchmark token"""
return equity_curve(
input,
benchmark_token_symbols=["ETH"],
)
def all_vaults_share_price_and_tvl(input: ChartInput) -> list[Figure]:
"""Limit max_count"""
return _all_vaults_share_price_and_tvl(
input,
max_count=2,
)
def create_charts(
timestamp: datetime.datetime | None,
parameters: StrategyParameters,
strategy_universe: TradingStrategyUniverse,
execution_context: ExecutionContext,
) -> ChartRegistry:
"""Define charts we use in backtesting and live trading."""
charts = ChartRegistry(default_benchmark_pairs=BENCHMARK_PAIRS)
charts.register(available_trading_pairs, ChartKind.indicator_all_pairs)
charts.register(inclusion_criteria_check, ChartKind.indicator_all_pairs)
charts.register(volatility_benchmark, ChartKind.indicator_multi_pair)
charts.register(signal_comparison, ChartKind.indicator_multi_pair)
charts.register(price_vs_signal, ChartKind.indicator_multi_pair)
charts.register(all_vaults_share_price_and_tvl, ChartKind.indicator_all_pairs)
charts.register(equity_curve_with_benchmark, ChartKind.state_all_pairs)
charts.register(equity_curve_with_drawdown, ChartKind.state_all_pairs)
charts.register(performance_metrics, ChartKind.state_all_pairs)
charts.register(volatile_weights_by_percent, ChartKind.state_all_pairs)
charts.register(volatile_and_non_volatile_percent, ChartKind.state_all_pairs)
charts.register(equity_curve_by_asset, ChartKind.state_all_pairs)
charts.register(weight_allocation_statistics, ChartKind.state_all_pairs)
charts.register(positions_at_end, ChartKind.state_all_pairs)
charts.register(last_messages, ChartKind.state_all_pairs)
charts.register(alpha_model_diagnostics, ChartKind.state_all_pairs)
charts.register(trading_pair_breakdown, ChartKind.state_all_pairs)
charts.register(trading_metrics, ChartKind.state_all_pairs)
charts.register(lending_pool_interest_accrued, ChartKind.state_all_pairs)
charts.register(vault_statistics, ChartKind.state_all_pairs)
charts.register(vault_position_timeline, ChartKind.state_single_vault_pair)
charts.register(all_vault_positions, ChartKind.state_single_vault_pair)
charts.register(trading_pair_positions, ChartKind.state_single_vault_pair)
charts.register(trading_pair_price_and_trades, ChartKind.state_single_vault_pair)
charts.register(inclusion_criteria_check, ChartKind.indicator_all_pairs)
return charts
#
# Metadata
#
tags = {StrategyTag.beta, StrategyTag.live, StrategyTag.deposits_disabled}
name = "Master vault strategy"
short_description = "Multi-vault allocation strategy on Arbitrum"
icon = ""
long_description = """
# Vault of vaults strategy
A diversified yield strategy that allocates across multiple DeFi vaults on Arbitrum.
## Strategy features
- **Multi-vault allocation**: Invests in 4 best-performing vaults from a universe of 16+ vaults
- **Weekly rebalancing**: Adjusts positions based on rolling 32-day returns
- **Risk management**: Caps individual positions at 25% of portfolio and 33% of pool TVL
- **TVL filtering**: Only considers vaults with at least $50,000 TVL
- **Denomination flexibility**: Supports USDC, USDT, USDC.e, crvUSD, USDai, and USD₮0
## Vault universe
The strategy selects from vaults including:
- Gains Network (gTrade)
- GMX vaults
- Morpho vaults
- Euler vaults
- And others
## Risk parameters
- Maximum 4 positions at any time
- 95% allocation target
- Minimum $500 per trade
- 25% maximum concentration per asset
"""