def _ideal_fiat_value_per_coin(self, market_state: MarketState) -> float: """We define "ideal" value as total value (in fiat) / # of available coins (including fiat). """ total_value = market_state.estimate_total_value( market_state.balances, self.fiat) num_coins = len(market_state.available_coins()) return total_value / num_coins
def reify_trade( cls, trade: AbstractTrade, market_state: MarketState, ) -> List[Order]: """Given an abstract trade, return a list of concrete orders that will accomplish the higher-level transaction described. """ markets = market_state.available_markets() market = format_currency_pair(trade.sell_coin, trade.buy_coin) if market not in markets: market = format_currency_pair(trade.buy_coin, trade.sell_coin) if market not in markets: raise NoMarketAvailableError( f'No market available between {trade.sell_coin} and ' f'{trade.buy_coin} and indirect trades are not yet supported' ) base, quote = split_currency_pair(market) # Price is given as base currency / quote currency price = market_state.price(market) # Order amount is given with respect to the quote currency quote_amount = market_state.estimate_value( trade.reference_coin, trade.reference_value, quote, ) # Order direction is given with respect to the quote currency if trade.sell_coin == base: # Buy quote currency; sell base currency direction = Order.Direction.BUY elif trade.sell_coin == quote: # Sell quote currency; buy quote currency direction = Order.Direction.SELL else: raise return [ Order( market, price, quote_amount, direction, OrderType.fill_or_kill, ) ]
def sell_to_achieve_value_of( self, desired_value: float, market_state: MarketState, ) -> None: ''' Sets `self.sell_amount`, `self.buy_amount`, `self.price` such that the proposed trade would leave us with a holding of `desired_value`. ''' self.estimate_price(market_state) if not self.price: logger.error( 'Must set a price for ProposedTrade, or pass a chart object ' 'into estimate_price_with') raise # After rebalance, we want the value of the coin we're trading to # to be equal to the ideal value (in fiat). # First we'll find the value of the coin we currently hold. current_value = market_state.balance(self.sell_coin) * self.price # To find how much coin we want to sell, # we'll subtract our holding's value from the ideal value # to produce the value of coin we must sell value_to_sell = current_value - desired_value # Now we find the amount of coin equal to this value amount_to_sell = value_to_sell / self.price self.set_sell_amount(amount_to_sell, market_state)
def test_simulate_trades(): trades = [ # Sell 0.5 BTC worth of BTC to buy ETH # -0.5 BTC, +6.737858883631113 ETH AbstractTrade('BTC', 'ETH', 'BTC', 0.5), # Sell 5 BCH worth of BTC to buy ETH # -0.60083005 BTC, +8.096616179890052 ETH AbstractTrade('BTC', 'ETH', 'BCH', 5), # Sell 1 ETH worth of ETH to buy BCH # -1 ETH, +0.612798695395699 BCH AbstractTrade('ETH', 'BCH', 'ETH', 1), ] chart_data = { 'BTC_ETH': { 'weighted_average': 0.07420755 }, # BTC/ETH 'BTC_BCH': { 'weighted_average': 0.12016601 }, # BTC/BCH 'ETH_BCH': { 'weighted_average': 1.63185726 }, # ETH/BCH } balances = {'BTC': 8} state = MarketState(chart_data, balances, datetime.now(), 'BTC') result = simulate_trades(trades, state) assert result == { 'BCH': 0.612798695395699, 'BTC': 6.89916995, 'ETH': 13.834475063521165, }
def propose_trades_for_total_rebalancing( self, market_state: MarketState, ) -> List[AbstractTrade]: """A total rebalancing should get us as close as possible to an equal distribution of value (w/r/t `self.fiat`) across all "reachable" markets (those in which the base currency is `self.fiat`). """ ideal_fiat_value_per_coin = self._ideal_fiat_value_per_coin( market_state) est_values = market_state.estimate_values(market_state.balances, self.fiat) coins_to_sell = {} coins_to_buy = {} for coin in sorted(self._possible_investments(market_state)): value = est_values.get(coin, 0) delta = value - ideal_fiat_value_per_coin if delta > 0: coins_to_sell[coin] = delta elif delta < 0: coins_to_buy[coin] = abs(delta) trades_to_fiat = [ AbstractTrade(sell_coin, self.fiat, self.fiat, fiat_value) for sell_coin, fiat_value in coins_to_sell.items() ] trades_from_fiat = [ AbstractTrade(self.fiat, buy_coin, self.fiat, fiat_value) for buy_coin, fiat_value in coins_to_buy.items() ] return trades_to_fiat + trades_from_fiat
def _possible_investments(self, market_state: MarketState) -> FrozenSet[str]: ''' Returns a set of all coins that the strategy might invest in, excluding `self.fiat`. ''' return market_state.available_coins() - {self.fiat}
def __init__( self, history: MarketHistory, initial_balances: Dict[str, float], fiat: str, ) -> None: self.market_history = history self.fiat = fiat self.market_state = MarketState(None, initial_balances, None, self.fiat)
def state(): chart_data = { 'BTC_ETH': { 'weighted_average': 0.07096974 }, 'ETH_BCH': { 'weighted_average': 1.84201100 }, } balances = {} return MarketState(chart_data, balances, datetime.now(), 'BTC')
def market_state(): chart_data = { 'BTC_ETH': { 'weighted_average': 0.07420755 }, # BTC/ETH 'BTC_BCH': { 'weighted_average': 0.12016601 }, # BTC/BCH 'ETH_BCH': { 'weighted_average': 1.63185726 }, # ETH/BCH } return MarketState(chart_data, {}, datetime.now(), 'BTC')
def rebalancing_proposed_trades( self, coins_to_rebalance: List[str], market_state: MarketState, ) -> List[ProposedTrade]: possible_investments = self._possible_investments(market_state) total_value = market_state.estimate_total_value() ideal_fiat_value_per_coin = total_value / len(possible_investments) proposed_trades_to_fiat = list( self._propose_trades_to_fiat(coins_to_rebalance, ideal_fiat_value_per_coin, market_state)) # Next, we will simulate actually executing all of these trades # Afterward, we'll get some simulated balances est_bals_after_fiat_trades = market_state.simulate_trades( proposed_trades_to_fiat) if self.fiat in coins_to_rebalance and len( proposed_trades_to_fiat) > 0: fiat_after_trades = est_bals_after_fiat_trades[self.fiat] to_redistribute = fiat_after_trades - ideal_fiat_value_per_coin coins_divested_from = [ proposed.sell_coin for proposed in proposed_trades_to_fiat ] coins_to_buy = possible_investments - set(coins_divested_from) - { self.fiat } to_redistribute_per_coin = to_redistribute / len(coins_to_buy) proposed_trades_from_fiat = self._propose_trades_from_fiat( coins_to_buy, to_redistribute_per_coin, market_state) trades = proposed_trades_to_fiat + list(proposed_trades_from_fiat) return trades return proposed_trades_to_fiat
def simulate_trades( trades: List[AbstractTrade], market_state: MarketState, ) -> Dict[str, float]: """ """ new = market_state.balances.copy() for trade in trades: sell_amount = market_state.estimate_value( trade.reference_coin, trade.reference_value, trade.sell_coin, ) new[trade.sell_coin] = new.get(trade.sell_coin, 0) - sell_amount buy_amount = market_state.estimate_value( trade.sell_coin, sell_amount, trade.buy_coin, ) new[trade.buy_coin] = new.get(trade.buy_coin, 0) + buy_amount return new
def estimate_price(self, market_state: MarketState): ''' Sets the approximate price of the quote value, given some chart data. ''' base_price = market_state.price(self.market_name) # The price (when buying/selling) # should match the self.market_name. # So, we keep around a self.market_price to match # self.price is always in the quote currency. self.market_price = base_price # Now, we find out what price matters for our trade. # The base price is always in the base currency, # So we will need to figure out if we are trading from, # or to, this base currency. if self.buy_coin == self.market_base_currency: self.price = 1 / base_price else: self.price = base_price
def get_market_state(self, time: datetime) -> MarketState: # Get the latest chart data from the market charts = self.market_history.latest(time) balances = self.get_balances() self.market_state = MarketState(charts, balances, time, self.fiat) return self.market_state
def propose_trades_for_partial_rebalancing( self, market_state: MarketState, coins_to_rebalance: FrozenSet[str], ) -> List[AbstractTrade]: """TODO: Trade directly from X to Y without going through fiat. """ ideal_fiat_value_per_coin = self._ideal_fiat_value_per_coin( market_state) est_values = market_state.estimate_values(market_state.balances, self.fiat) # 1) Fan in to fiat, selling excess value in coins we want to rebalance trades_to_fiat = [] for sell_coin in sorted(coins_to_rebalance): if sell_coin == self.fiat: continue value = est_values.get(sell_coin, 0) delta = value - ideal_fiat_value_per_coin if delta > 0: trades_to_fiat.append( AbstractTrade(sell_coin, self.fiat, self.fiat, delta), ) # 2) Simulate trades and estimate portfolio state afterwards est_balances_after_trades = simulate_trades( trades_to_fiat, market_state, ) est_values_after_trades = market_state.estimate_values( est_balances_after_trades, self.fiat, ) fiat_after_trades = est_balances_after_trades[self.fiat] fiat_to_redistribute = fiat_after_trades - ideal_fiat_value_per_coin if fiat_to_redistribute <= 0: return trades_to_fiat # 3) Find coins in which we don't hold enough value possible_buys = set() for buy_coin in self._possible_investments(market_state): value = est_values_after_trades.get(buy_coin, 0) if ideal_fiat_value_per_coin > value: possible_buys.add(buy_coin) fiat_to_redistribute_per_coin = fiat_to_redistribute / len( possible_buys) # 4) Plan trades, fanning back out from fiat to others trades_from_fiat = [] for buy_coin in sorted(possible_buys): value = est_values_after_trades.get(buy_coin, 0) delta = ideal_fiat_value_per_coin - value if delta > 0: available_fiat = min(fiat_to_redistribute_per_coin, delta) trades_from_fiat.append( AbstractTrade(self.fiat, buy_coin, self.fiat, available_fiat), ) return trades_to_fiat + trades_from_fiat