diff --git a/vega_query/checks/fees.py b/vega_query/checks/fees.py new file mode 100644 index 000000000..026159fd5 --- /dev/null +++ b/vega_query/checks/fees.py @@ -0,0 +1,465 @@ +""" +""" + +from decimal import Decimal +from collections import defaultdict +from typing import List, Dict, Optional + +from vega_query.service.service import Service, Network + +import vega_protos as protos + + +def ceil(value: Decimal) -> Decimal: + return Decimal(value.__ceil__()) + + +def floor(value: Decimal) -> Decimal: + return Decimal(value.__floor__()) + + +class Checker: + + KEYS = ( + "market.fee.factors.makerFee", + "market.fee.factors.buybackFee", + "market.fee.factors.treasuryFee", + "market.fee.factors.infrastructureFee", + ) + + def __init__(self, service: Service, acceptable_error=Decimal("5")) -> None: + + self.acceptable_error = acceptable_error + + self.__service = service + self.__trades: List[protos.vega.vega.Trade] = None + self.__assets: Dict[str, protos.vega.assets.Asset] = None + self.__markets: Dict[str, protos.vega.markets.Market] = None + + self.__epochs: List[protos.vega.vega.Epoch] = [] + + self.__fee_factors: Dict[int, List[Dict[str, str]]] = defaultdict(list) + + self.__volume_discount_factors: Dict[ + int, Dict[str, protos.vega.vega.DiscountFactors] + ] = defaultdict(dict) + self.__volume_rebate_factors: Dict[int, Dict[str, str]] = defaultdict(dict) + + self.__initialise_epochs() + self.__initialise_trades() + self.__initialise_network_parameters() + + @property + def auction(self) -> bool: + return self.__trade.aggressor is None + + @property + def markets(self) -> Dict[str, protos.vega.markets.Market]: + if self.__markets is None: + self.__markets = { + market.id: market for market in self.__service.api.data.list_markets() + } + return self.__markets + + @property + def assets(self) -> Dict[str, protos.vega.assets.Asset]: + if self.__assets is None: + self.__assets = { + market.id: self.__service.utils.market.find_asset( + market.tradable_instrument.instrument.code + ) + for market in self.markets.values() + } + return self.__assets + + @property + def trades(self) -> List[protos.vega.vega.Trade]: + if self.__trades is None: + return None + return self.__trades + + @property + def fee_factors(self): + return self.__fee_factors + + def get_party_referral_reward_factors(self, epoch: int, party: str): + return None + + def get_party_referral_discount_factors(self, epoch: int, party: str): + return None + + def get_party_volume_discount_factors( + self, epoch: int, party: str + ) -> Optional[protos.vega.vega.DiscountFactors]: + discount_factors = self.__volume_discount_factors[epoch].get(party, None) + if discount_factors: + return discount_factors + stats = self.__service.api.data.get_volume_discount_stats( + at_epoch=epoch, party_id=party + ) + if len(stats) > 0: + self.__volume_discount_factors[epoch][party] = stats[0].discount_factors + return self.__volume_discount_factors[epoch][party] + return None + + def get_party_volume_rebate_factor(self, epoch: int, party: str) -> Decimal: + rebate_factor = self.__volume_rebate_factors[epoch].get(party, None) + if rebate_factor: + return Decimal(rebate_factor) + stats = self.__service.api.data.get_volume_rebate_stats( + at_epoch=epoch, party_id=party + ) + if len(stats) > 0: + self.__volume_rebate_factors[epoch][party] = stats[ + 0 + ].additional_maker_rebate + return Decimal(self.__volume_rebate_factors[epoch][party]) + return Decimal("0") + + def get_epoch(self, ts: int) -> protos.vega.vega.Epoch: + for epoch in self.__epochs: + if epoch.timestamps.start_time <= ts: + return epoch + while not epoch.timestamps.start_time <= ts: + epoch = self.__service.api.data.get_epoch(id=(epoch.seq - 1)) + if epoch is None: + return None + self.__epochs.append(epoch) + return epoch + + def get_fee_factor(self, key: str, timestamp: int) -> Decimal: + if key not in self.KEYS: + raise ValueError(f"invalid key {key}") + current = self.fee_factors[key][0] + for param in self.fee_factors[key]: + if timestamp > param["ts"]: + break + current = param + return Decimal(current["value"]) + + def __initialise_epochs(self): + self.__epochs.append(self.__service.api.data.get_epoch()) + + def __initialise_trades(self): + self.__trades = self.__service.api.data.list_trades( + date_range_start_timestamp=self.__epochs[0].timestamps.end_time, + max_pages=5, + ) + + def __initialise_network_parameters(self): + ts = self.__service.api.data.get_vega_time() + for key in self.KEYS: + self.__fee_factors[key].append( + { + "ts": ts, + "value": self.__service.api.data.get_network_parameter( + key=key + ).value, + } + ) + for governance_data in self.__service.api.data.list_governance_data( + proposal_state=protos.vega.governance.Proposal.STATE_ENACTED, + proposal_type=protos.data_node.api.v2.trading_data.ListGovernanceDataRequest.TYPE_NETWORK_PARAMETERS, + ): + if governance_data.proposal is not None: + ts = governance_data.proposal.timestamp + key = ( + governance_data.proposal.terms.update_network_parameter.changes.key + ) + value = ( + governance_data.proposal.terms.update_network_parameter.changes.value + ) + if key in self.KEYS: + self.__fee_factors[key].append(({"ts": ts, "value": value})) + + def check(self): + checked = 0 + for trade in self.trades: + self.__check_trade(trade) + checked += 1 + if checked % 10 == 0: + print(f"checked {checked} of {checked} trades") + print(f"finished and checked {checked} trades") + + def __check_trade(self, trade: protos.vega.vega.Trade): + + if trade.type == protos.vega.vega.Trade.TYPE_NETWORK_CLOSE_OUT_BAD: + return True + if trade.type == protos.vega.vega.Trade.TYPE_NETWORK_CLOSE_OUT_GOOD: + return True + if trade.aggressor: + self.__handle_continuous(trade) + else: + self.__handle_auction(trade) + + def get_notional(self, trade: protos.vega.vega.Trade) -> Decimal: + asset = self.assets[trade.market_id] + market = self.markets[trade.market_id] + return Decimal( + int(trade.price) + * int(trade.size) + * Decimal(10) ** -Decimal(market.decimal_places) + * Decimal(10) ** -Decimal(market.position_decimal_places) + * Decimal(10) ** +Decimal(asset.details.decimals) + ) + + def apply_reward_factors( + self, + maker_fee: Decimal, + liquidity_fee: Decimal, + infrastructure_fee: Decimal, + reward_factors: Optional[protos.vega.vega.RewardFactors] = None, + ): + if reward_factors is None: + return ( + maker_fee, + liquidity_fee, + infrastructure_fee, + Decimal(0), + Decimal(0), + Decimal(0), + ) + if any( + not isinstance(x, Decimal) + for x in ( + maker_fee, + liquidity_fee, + infrastructure_fee, + ) + ): + raise ValueError("all inputs must be decimal") + maker_reward = floor(maker_fee * Decimal(reward_factors.maker_reward_factor)) + liquidity_reward = floor( + liquidity_fee * Decimal(reward_factors.liquidity_reward_factor) + ) + infrastructure_reward = floor( + infrastructure_fee * Decimal(reward_factors.infrastructure_reward_factor) + ) + return ( + maker_fee - maker_reward, + liquidity_fee - liquidity_reward, + infrastructure_fee - infrastructure_reward, + maker_reward, + liquidity_reward, + infrastructure_reward, + ) + + def apply_discount_factors( + self, + maker_fee: Decimal, + liquidity_fee: Decimal, + infrastructure_fee: Decimal, + discount_factors: Optional[protos.vega.vega.DiscountFactors] = None, + ): + if discount_factors is None: + return ( + maker_fee, + liquidity_fee, + infrastructure_fee, + Decimal(0), + Decimal(0), + Decimal(0), + ) + if any( + not isinstance(x, Decimal) + for x in ( + maker_fee, + liquidity_fee, + infrastructure_fee, + ) + ): + raise ValueError("all inputs must be decimal") + maker_discount = floor( + maker_fee * Decimal(discount_factors.maker_discount_factor) + ) + liquidity_discount = floor( + liquidity_fee * Decimal(discount_factors.liquidity_discount_factor) + ) + infrastructure_discount = floor( + infrastructure_fee + * Decimal(discount_factors.infrastructure_discount_factor) + ) + return ( + maker_fee - maker_discount, + liquidity_fee - liquidity_discount, + infrastructure_fee - infrastructure_discount, + maker_discount, + liquidity_discount, + infrastructure_discount, + ) + + def apply_rebate_factor( + self, + notional: Decimal, + buyback_fee: Decimal, + treasury_fee: Decimal, + additional_maker_rebate: Optional[Decimal] = None, + ): + if additional_maker_rebate is None: + return buyback_fee, treasury_fee, Decimal(0) + if any( + not isinstance(x, Decimal) + for x in (notional, buyback_fee, treasury_fee, additional_maker_rebate) + ): + raise ValueError("all inputs must be decimal") + high_volume_maker_fee = floor(notional * additional_maker_rebate) + factor = Decimal(1) - (high_volume_maker_fee / (buyback_fee + treasury_fee)) + return ( + floor(buyback_fee * factor), + floor(treasury_fee * factor), + high_volume_maker_fee, + ) + + def __assert(self, a: Decimal, b: Decimal): + err = a - b + if abs(err) > self.acceptable_error: + raise AssertionError( + f"error: {err} > {self.acceptable_error} {{a: {a}, b: {b}}}" + ) + + def __handle_auction(self, trade): + pass + + def __handle_continuous(self, trade: protos.vega.vega.Trade): + + fees = ( + trade.buyer_fee + if trade.aggressor == protos.vega.vega.SIDE_BUY + else trade.seller_fee + ) + taker = ( + trade.buyer + if trade.aggressor == protos.vega.vega.SIDE_BUY + else trade.seller + ) + maker = ( + trade.seller + if trade.aggressor == protos.vega.vega.SIDE_BUY + else trade.buyer + ) + + epoch = self.get_epoch(trade.timestamp) + + notional = self.get_notional(trade) + + referral_reward_factors = self.get_party_referral_reward_factors( + epoch=epoch.seq - 1, party=taker + ) + referral_discount_factors = self.get_party_referral_discount_factors( + epoch=epoch.seq - 1, party=taker + ) + volume_discount_factors = self.get_party_volume_discount_factors( + epoch=epoch.seq - 1, party=taker + ) + + # Calculate full fees + maker_fee_factor = self.get_fee_factor( + "market.fee.factors.makerFee", trade.timestamp + ) + liquidity_fee_factor = Decimal("0") + infrastructure_fee_factor = self.get_fee_factor( + "market.fee.factors.infrastructureFee", trade.timestamp + ) + buyback_fee_factor = self.get_fee_factor( + "market.fee.factors.buybackFee", trade.timestamp + ) + treasury_fee_factor = self.get_fee_factor( + "market.fee.factors.treasuryFee", trade.timestamp + ) + maker_fee = ceil(notional * maker_fee_factor) + liquidity_fee = ceil(notional * liquidity_fee_factor) + infrastructure_fee = ceil(notional * infrastructure_fee_factor) + buyback_fee = ceil(notional * buyback_fee_factor) + treasury_fee = ceil(notional * treasury_fee_factor) + + # Apply the referral rewards + ( + maker_fee, + liquidity_fee, + infrastructure_fee, + maker_fee_referral_reward, + liquidity_fee_referral_reward, + infrastructure_fee_referral_reward, + ) = self.apply_reward_factors( + maker_fee, liquidity_fee, infrastructure_fee, referral_reward_factors + ) + + # Apply the referral discount + ( + maker_fee, + liquidity_fee, + infrastructure_fee, + maker_fee_referral_discount, + liquidity_fee_referral_discount, + infrastructure_fee_referral_discount, + ) = self.apply_discount_factors( + maker_fee, liquidity_fee, infrastructure_fee, referral_discount_factors + ) + + # Apply the volume discounts + ( + maker_fee, + liquidity_fee, + infrastructure_fee, + maker_fee_volume_discount, + liquidity_fee_volume_discount, + infrastructure_fee_volume_discount, + ) = self.apply_discount_factors( + maker_fee, liquidity_fee, infrastructure_fee, volume_discount_factors + ) + + # Apply the rebate factors + volume_rebate_factor = self.get_party_volume_rebate_factor( + epoch=epoch.seq - 1, party=maker + ) + effective_rebate_factor = min( + volume_rebate_factor if volume_rebate_factor else Decimal("0"), + buyback_fee_factor + treasury_fee_factor, + ) + ( + buyback_fee, + treasury_fee, + high_volume_maker_fee, + ) = self.apply_rebate_factor( + notional, buyback_fee, treasury_fee, effective_rebate_factor + ) + + # Check discounts first + self.__assert( + Decimal(fees.maker_fee_volume_discount), maker_fee_volume_discount + ) + self.__assert( + Decimal(fees.infrastructure_fee_volume_discount), + infrastructure_fee_volume_discount, + ) + self.__assert( + Decimal(fees.maker_fee_referrer_discount), maker_fee_referral_discount + ) + self.__assert( + Decimal(fees.infrastructure_fee_referrer_discount), + infrastructure_fee_referral_discount, + ) + self.__assert(Decimal(maker_fee_referral_reward), maker_fee_referral_reward) + self.__assert( + Decimal(infrastructure_fee_referral_reward), + infrastructure_fee_referral_reward, + ) + self.__assert(Decimal(fees.high_volume_maker_fee), high_volume_maker_fee) + + # Check final fee components + self.__assert(Decimal(fees.maker_fee), maker_fee) + self.__assert(Decimal(fees.buy_back_fee), buyback_fee) + self.__assert(Decimal(fees.treasury_fee), treasury_fee) + self.__assert(Decimal(fees.infrastructure_fee), infrastructure_fee) + + +if __name__ == "__main__": + + import logging + + logging.basicConfig(level=logging.INFO) + + # s = Service(network=Network.NETWORK_TESTNET) + s = Service(network=Network.NETWORK_LOCAL, port_data_node=57523) + c = Checker(s) + c.check() diff --git a/vega_sim/configs/agents.py b/vega_sim/configs/agents.py index bcc9ef5e9..817833411 100644 --- a/vega_sim/configs/agents.py +++ b/vega_sim/configs/agents.py @@ -178,7 +178,7 @@ def __find_or_create_asset(self, symbol: str): wallet_name=self.wallet_name, symbol=symbol, name=symbol, - decimals=18, + decimals=6, quantum=int(10**18), ) asset_id = self.vega.find_asset_id(