Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion meta/meta.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
"subnet_version": "6.1.0"
"subnet_version": "6.1.1"
}
4 changes: 2 additions & 2 deletions neurons/validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -761,8 +761,8 @@ def receive_signal(self, synapse: template.protocol.SendSignal,
order.slippage = PriceSlippageModel.calculate_slippage(order.bid, order.ask, order)
self._enforce_num_open_order_limit(trade_pair_to_open_position, order)
self.enforce_order_cooldown(order, existing_position)
net_portfolio_leverage = self.position_manager.calculate_net_portfolio_leverage(miner_hotkey)
existing_position.add_order(order, net_portfolio_leverage)
net_portfolio_leverage, net_currency_leverage = self.position_manager.calculate_net_portfolio_leverage(miner_hotkey)
existing_position.add_order(order, net_portfolio_leverage, net_currency_leverage)
self.position_manager.save_miner_position(existing_position)
synapse.order_json = order.__str__()
if miner_order_uuid:
Expand Down
49 changes: 44 additions & 5 deletions tests/vali_tests/test_positions.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ def setUp(self):
self.position_manager.clear_all_miner_positions()

def add_order_to_position_and_save(self, position, order):
position.add_order(order, self.position_manager.calculate_net_portfolio_leverage(self.DEFAULT_MINER_HOTKEY))
net_portfolio_leverage, net_currency_leverage = self.position_manager.calculate_net_portfolio_leverage(self.DEFAULT_MINER_HOTKEY)
position.add_order(order, net_portfolio_leverage, net_currency_leverage)
self.position_manager.save_miner_position(position)

def _find_disk_position_from_memory_position(self, position):
Expand Down Expand Up @@ -2196,7 +2197,7 @@ def test_long_order_leverage_already_at_portfolio_limit(self):
order_uuid="1000")
self.add_order_to_position_and_save(position2, o2)
self.position_manager.save_miner_position(position2)
self.assertEqual(self.position_manager.calculate_net_portfolio_leverage(self.DEFAULT_MINER_HOTKEY), 10.0)
self.assertEqual(self.position_manager.calculate_net_portfolio_leverage(self.DEFAULT_MINER_HOTKEY)[0], 10.0)

position3 = deepcopy(self.default_position)
position3.trade_pair = TradePair.ETHUSD
Expand Down Expand Up @@ -2238,7 +2239,7 @@ def test_long_order_crypto_leverage_exceed_portfolio_limit(self):
self.add_order_to_position_and_save(position2, o2)
self.position_manager.save_miner_position(position2)

self.assertEqual(self.position_manager.calculate_net_portfolio_leverage(self.DEFAULT_MINER_HOTKEY), 9.0)
self.assertEqual(self.position_manager.calculate_net_portfolio_leverage(self.DEFAULT_MINER_HOTKEY)[0], 9.0)

position3 = deepcopy(self.default_position)
position3.trade_pair=TradePair.ETHUSD
Expand Down Expand Up @@ -2282,7 +2283,7 @@ def test_long_order_forex_leverage_exceed_portfolio_limit(self):
order_uuid="1000")
self.add_order_to_position_and_save(position2, o2)
self.position_manager.save_miner_position(position2)
self.assertEqual(self.position_manager.calculate_net_portfolio_leverage(self.DEFAULT_MINER_HOTKEY), 9.0)
self.assertEqual(self.position_manager.calculate_net_portfolio_leverage(self.DEFAULT_MINER_HOTKEY)[0], 9.0)

position3 = deepcopy(self.default_position)
position3.trade_pair=TradePair.AUDJPY
Expand Down Expand Up @@ -2324,7 +2325,7 @@ def test_short_order_leverage_exceed_portfolio_limit(self):
processed_ms=leverage_utils.PORTFOLIO_LEVERAGE_BOUNDS_START_TIME_MS + 1000,
order_uuid="1000")
self.add_order_to_position_and_save(position2, o2)
self.assertEqual(self.position_manager.calculate_net_portfolio_leverage(self.DEFAULT_MINER_HOTKEY), 9.0)
self.assertEqual(self.position_manager.calculate_net_portfolio_leverage(self.DEFAULT_MINER_HOTKEY)[0], 9.0)

position3 = deepcopy(self.default_position)
position3.trade_pair = TradePair.ETHUSD
Expand Down Expand Up @@ -2380,6 +2381,44 @@ def test_position_below_min_while_portfolio_lev_exceeded(self):
# the o3 order should be skipped since it would bring the position net leverage below min leverage.
self.assertEqual(position2.net_leverage, 50)

def test_currency_net_leverage_exceeded(self):
"""
a fx position which brings one of either base or quote currencies above the currency net leverage limit
should not be allowed
"""
position1 = deepcopy(self.default_position)
position1.trade_pair = TradePair.USDJPY
o1 = Order(order_type=OrderType.LONG,
leverage=5,
price=100,
trade_pair=TradePair.USDJPY,
processed_ms=leverage_utils.CURRENCY_NET_LEVERAGE_BOUNDS_START_TIME_MS + 1000,
order_uuid="1000")
self.add_order_to_position_and_save(position1, o1)

position2 = deepcopy(self.default_position)
position2.trade_pair = TradePair.EURAUD
position2.position_uuid = self.DEFAULT_POSITION_UUID + "_2"
o2 = Order(order_type=OrderType.LONG,
leverage=1,
price=100,
trade_pair=TradePair.EURAUD,
processed_ms=leverage_utils.CURRENCY_NET_LEVERAGE_BOUNDS_START_TIME_MS + 1000,
order_uuid="1001")
self.add_order_to_position_and_save(position2, o2)

position3 = deepcopy(self.default_position)
position3.trade_pair = TradePair.USDCAD
position3.position_uuid = self.DEFAULT_POSITION_UUID + "_3"
o3 = Order(order_type=OrderType.LONG,
leverage=1,
price=100,
trade_pair=TradePair.USDCAD,
processed_ms=leverage_utils.CURRENCY_NET_LEVERAGE_BOUNDS_START_TIME_MS + 1000,
order_uuid="1002")
with self.assertRaises(ValueError):
# the o3 order should be skipped since it would bring the currency net leverage above the cap.
self.add_order_to_position_and_save(position3, o3)

def test_position_json(self):
position = deepcopy(self.default_position)
Expand Down
20 changes: 17 additions & 3 deletions vali_objects/position.py
Original file line number Diff line number Diff line change
Expand Up @@ -338,7 +338,7 @@ def log_position_status(self):
]
bt.logging.debug(f"position order details: " f"close_ms [{order_info}] ")

def add_order(self, order: Order, net_portfolio_leverage: float=0.0) -> bool:
def add_order(self, order: Order, net_portfolio_leverage: float=0.0, net_currency_leverage: dict=None):
"""
Add an order to a position, and adjust its leverage to stay within
the trade pair max and portfolio max.
Expand All @@ -349,7 +349,10 @@ def add_order(self, order: Order, net_portfolio_leverage: float=0.0) -> bool:
raise ValueError(
f"Order trade pair [{order.trade_pair}] does not match position trade pair [{self.trade_pair}]")

if self._clamp_and_validate_leverage(order, abs(net_portfolio_leverage)):
if net_currency_leverage is None:
net_currency_leverage = {}

if self._clamp_and_validate_leverage(order, abs(net_portfolio_leverage), net_currency_leverage):
# This order's leverage got clamped to zero.
# Skip it since we don't want to consider this a FLAT position and we don't want to allow bad actors
# to send in a bunch of spam orders.
Expand Down Expand Up @@ -601,14 +604,16 @@ def reopen_position(self):
self.is_closed_position = False
self.close_ms = None

def _clamp_and_validate_leverage(self, order: Order, net_portfolio_leverage: float) -> bool:
def _clamp_and_validate_leverage(self, order: Order, net_portfolio_leverage: float, net_currency_leverage: dict) -> bool:
"""
If an order's leverage would make the position's leverage higher than max_position_leverage,
we clamp the order's leverage. If clamping causes the order's leverage to be below
ValiConfig.ORDER_MIN_LEVERAGE, we raise an error.

If an order's leverage would take the position leverage below min_position_leverage, we raise an error.

For FX, order leverage must also stay within net currency leverage exposure limits.

Return true if the order should be ignored. Only happens when the order attempts to exceed max_position_leverage
and is already at max_position_leverage.
"""
Expand All @@ -626,6 +631,15 @@ def _clamp_and_validate_leverage(self, order: Order, net_portfolio_leverage: flo
(abs(proposed_leverage) * self.trade_pair.leverage_multiplier))
max_portfolio_leverage = leverage_utils.get_portfolio_leverage_cap(order.processed_ms)

if order.trade_pair.is_forex:
proposed_base_currency_leverage = net_currency_leverage.get(order.trade_pair.base, 0) + order.leverage
proposed_quote_currency_leverage = net_currency_leverage.get(order.trade_pair.quote, 0) - order.leverage
max_currency_leverage = leverage_utils.get_currency_net_leverage_cap(order.processed_ms, order.trade_pair)
if abs(proposed_base_currency_leverage) > max_currency_leverage:
raise ValueError(f"Miner {self.miner_hotkey} attempted to exceed {self.trade_pair.base} currency leverage exposure. Current exposure: {net_currency_leverage.get(order.trade_pair.base)}")
if abs(proposed_quote_currency_leverage) > max_currency_leverage:
raise ValueError(f"Miner {self.miner_hotkey} attempted to exceed {self.trade_pair.quote} currency leverage exposure. Current exposure: {net_currency_leverage.get(order.trade_pair.quote)}")

# we only need to worry about clamping if the sign of the position leverage remains the same i.e. position does not flip and close
if is_first_order or self.net_leverage * proposed_leverage > 0:
if (abs(proposed_leverage) > max_position_leverage
Expand Down
10 changes: 9 additions & 1 deletion vali_objects/utils/leverage_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
PORTFOLIO_LEVERAGE_BOUNDS_START_TIME_MS = 1727161200000
INDICES_SOFT_CUTOFF_MS = 1731700800000
EQUITIES_METALS_SOFT_CUTOFF_MS = 1744700399000
CURRENCY_NET_LEVERAGE_BOUNDS_START_TIME_MS = 1747551600000

from vali_objects.vali_config import TradePair, ValiConfig # noqa: E402

Expand Down Expand Up @@ -40,6 +41,13 @@ def get_position_leverage_bounds(trade_pair: TradePair, t_ms: int) -> (float, fl
return min_position_leverage, max_position_leverage

def get_portfolio_leverage_cap(t_ms: int) -> float:
is_portfolio_cap= t_ms >= PORTFOLIO_LEVERAGE_BOUNDS_START_TIME_MS
is_portfolio_cap = t_ms >= PORTFOLIO_LEVERAGE_BOUNDS_START_TIME_MS
max_portfolio_leverage = ValiConfig.PORTFOLIO_LEVERAGE_CAP if is_portfolio_cap else float('inf')
return max_portfolio_leverage

def get_currency_net_leverage_cap(t_ms: int, trade_pair: TradePair) -> float:
if not trade_pair.is_forex:
return float('inf')
is_currency_cap = t_ms >= CURRENCY_NET_LEVERAGE_BOUNDS_START_TIME_MS
max_currency_leverage = ValiConfig.CURRENCY_NET_LEVERAGE_CAP if is_currency_cap else float('inf')
return max_currency_leverage
8 changes: 6 additions & 2 deletions vali_objects/utils/position_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -931,18 +931,22 @@ def _delete_position_from_memory(self, hotkey, position_uuid):
else:
del self.hotkey_to_positions[hotkey]

def calculate_net_portfolio_leverage(self, hotkey: str) -> float:
def calculate_net_portfolio_leverage(self, hotkey: str) -> (float, dict):
"""
Calculate leverage across all open positions
Normalize each asset class with a multiplier
"""
positions = self.get_positions_for_one_hotkey(hotkey, only_open_positions=True)
currency_net_leverage = defaultdict(float)

portfolio_leverage = 0.0
for position in positions:
portfolio_leverage += abs(position.get_net_leverage()) * position.trade_pair.leverage_multiplier
if position.trade_pair.is_forex:
currency_net_leverage[position.trade_pair.base] += position.get_net_leverage()
currency_net_leverage[position.trade_pair.quote] -= position.get_net_leverage()

return portfolio_leverage
return portfolio_leverage, currency_net_leverage

@timeme
def get_positions_for_all_miners(self, from_disk=False, **args):
Expand Down
17 changes: 14 additions & 3 deletions vali_objects/vali_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@ class ValiConfig:
EQUITIES_MIN_LEVERAGE = 0.1
EQUITIES_MAX_LEVERAGE = 3

# Cap leverage across miner's entire portfolio
PORTFOLIO_LEVERAGE_CAP = 10
CURRENCY_NET_LEVERAGE_CAP = 5

CAPITAL = 100_000 # conversion of 1x leverage to $100K in capital

MAX_DAILY_DRAWDOWN = 0.95 # Portfolio should never fall below .95 x of initial value when measured day to day
Expand Down Expand Up @@ -161,9 +165,6 @@ class ValiConfig:
# Require at least this many successful checkpoints before building golden
MIN_CHECKPOINTS_RECEIVED = 5

# Cap leverage across miner's entire portfolio
PORTFOLIO_LEVERAGE_CAP = 10

assert ValiConfig.CRYPTO_MIN_LEVERAGE >= ValiConfig.ORDER_MIN_LEVERAGE
assert ValiConfig.CRYPTO_MAX_LEVERAGE <= ValiConfig.ORDER_MAX_LEVERAGE
assert ValiConfig.FOREX_MIN_LEVERAGE >= ValiConfig.ORDER_MIN_LEVERAGE
Expand Down Expand Up @@ -322,6 +323,16 @@ def leverage_multiplier(self) -> int:
TradePairCategory.EQUITIES: 2}
return trade_pair_leverage_multiplier[self.trade_pair_category]

@property
def base(self):
if self.is_forex:
return self.trade_pair.split("/")[0]

@property
def quote(self):
if self.is_forex:
return self.trade_pair.split("/")[1]

@classmethod
def categories(cls):
return {tp.trade_pair_id: tp.trade_pair_category.value for tp in cls}
Expand Down