def test_valid_isub(): # Add to remove from unlocked balance wallet = Wallet(exchange, 10000 * USD) wallet.withdraw( quantity=500 * USD, reason="test" ) assert wallet.balance == 9500 * USD assert len(wallet.locked) == 0 # Add to balance with locked path_id wallet = Wallet(exchange, 10000 * USD) wallet.deposit( quantity=Quantity(USD, 750, path_id=path_id), reason="test" ) wallet.deposit( quantity=Quantity(USD, 1000, path_id=other_id), reason="test" ) wallet.withdraw( quantity=Quantity(USD, 500, path_id=path_id), reason="test" ) assert wallet.balance == 10000 * USD assert wallet.locked[path_id] == 250 * USD wallet.withdraw( quantity=Quantity(USD, 500, path_id=other_id), reason="test" ) assert wallet.balance == 10000 * USD assert wallet.locked[other_id] == 500 * USD
def test_total_balance(): wallet = Wallet(exchange, 10000 * USD) wallet.deposit(quantity=Quantity(USD, 500, path_id=path_id), reason="test") wallet.deposit(quantity=Quantity(USD, 700, path_id=other_id), reason="test") assert wallet.total_balance == 11200 * USD
def BinancePortfolio(exchange): # setup instruments. Must include base currency and your crypto instruments = [Quantity(USD, 10000), Quantity(BTC, 1)] # setup wallet with instruments wallets = [] for instrument in instruments: wallets.append(Wallet(exchange, instrument)) # define portfolio with a wallet portfolio = Portfolio(base_instrument=USD, wallets=wallets) return portfolio
def execute_close_order(order: 'Order', base_wallet: 'Wallet', quote_wallet: 'Wallet', current_price: float, options: 'ExchangeOptions', clock: 'Clock') -> 'Trade': """Executes a sell order on the exchange. Parameters ---------- order : `Order` The order that is being filled. base_wallet : `Wallet` The wallet of the base instrument. quote_wallet : `Wallet` The wallet of the quote instrument. current_price : float The current price of the exchange pair. options : `ExchangeOptions` The exchange options. clock : `Clock` The clock for the trading process.. Returns ------- `Trade` The executed trade that was made. """ if order.type == TradeType.LIMIT and order.price > current_price: return None filled = order.remaining.contain(order.exchange_pair) commission = Quantity(instrument=filled.instrument, size=0) quantity = filled - commission ''' # Transfer Funds from Quote Wallet to Base Wallet transfer = Wallet.transfer( source=quote_wallet, target=base_wallet, quantity=quantity, commission=commission, exchange_pair=order.exchange_pair, reason="CLOSE" ) ''' # to be replaced by broker position = quote_wallet.get_positions()[order.path_id] if position: # mark position as closed position.is_open = False trade = Trade(order_id=order.id, step=clock.step, exchange_pair=order.exchange_pair, side=order.side, trade_type=order.type, quantity=order.quantity, price=order.price, commission=commission) return trade
def total_worth_quantity(self) -> 'Quantity': """The total worth quantity of the wallet based on positions. (`Quantity`, read-only)""" total_worth_quantity = 0 for position in self._positions.values(): total_worth_quantity += position.get_worth_quantity() return Quantity(instrument=self.instrument, size=total_worth_quantity)
def locked_balance(self) -> 'Quantity': """The total balance of the wallet locked in orders. (`Quantity`, read-only)""" locked_balance = Quantity(self.instrument, 0) for quantity in self._locked.values(): locked_balance += quantity.size return locked_balance
def test_invalid_isub(): # Add to balance with locked path_id wallet = Wallet(exchange, 10000 * USD) wallet.deposit(quantity=Quantity(USD, 500, path_id=path_id), reason="test") wallet.deposit(quantity=Quantity(USD, 700, path_id=other_id), reason="test") with pytest.raises(InsufficientFunds): wallet.withdraw(quantity=11000 * USD, reason="test") with pytest.raises(InsufficientFunds): wallet.withdraw(quantity=Quantity(USD, 750, path_id), reason="test") with pytest.raises(InsufficientFunds): wallet.withdraw(quantity=Quantity(USD, 750, path_id), reason="test") with pytest.raises(IncompatibleInstrumentOperation): wallet.withdraw(quantity=500 * BTC, reason="test")
def test_valid_deposit(): # Add to free unlocked balance wallet = Wallet(exchange, 10000 * USD) wallet.deposit(500 * USD, reason="test") assert wallet.balance == 10500 * USD assert len(wallet.locked) == 0 # Add to balance with locked path_id wallet = Wallet(exchange, 10000 * USD) wallet.deposit(Quantity(USD, 500, path_id=path_id), reason="test") assert wallet.balance == 10000 * USD assert wallet.locked[path_id] == 500 * USD # Add to more balance with locked path_id wallet.deposit(Quantity(USD, 500, path_id=path_id), reason="test") assert wallet.balance == 10000 * USD assert wallet.locked[path_id] == 1000 * USD # Add to balance that has another locked path_id wallet.deposit(Quantity(USD, 500, path_id=other_id), reason="test") assert wallet.balance == 10000 * USD assert wallet.locked[path_id] == 1000 * USD assert wallet.locked[other_id] == 500 * USD
def from_tuple(cls, wallet_tuple: 'Tuple[Exchange, Instrument, float]') -> 'Wallet': """Creates a wallet from a wallet tuple. Parameters ---------- wallet_tuple : `Tuple[Exchange, Instrument, float]` A tuple containing an exchange, instrument, and amount. Returns ------- `Wallet` A wallet corresponding to the arguments given in the tuple. """ exchange, instrument, balance = wallet_tuple return cls(exchange, Quantity(instrument, balance))
def locked_balance(self, instrument: Instrument) -> 'Quantity': """Gets the total balance a specific instrument locked in orders over the entire portfolio. Parameters ---------- instrument : `Instrument` The instrument to find locked balances for. Returns ------- `Quantity` The total locked balance of the instrument. """ balance = Quantity(instrument, 0) for (_, symbol), wallet in self._wallets.items(): if symbol == instrument.symbol: balance += wallet.locked_balance return balance
def balance(self, instrument: Instrument) -> 'Quantity': """Gets the total balance of the portfolio in a specific instrument available for use. Parameters ---------- instrument : `Instrument` The instrument to compute the balance for. Returns ------- `Quantity` The balance of the instrument over all wallets. """ balance = Quantity(instrument, 0) for (_, symbol), wallet in self._wallets.items(): if symbol == instrument.symbol: balance += wallet.balance return balance
def test_exchange_with_wallets_feed(): ex1 = Exchange("bitfinex", service=execute_order)( Stream.source([7000, 7500, 8300], dtype="float").rename("USD-BTC"), Stream.source([200, 212, 400], dtype="float").rename("USD-ETH")) ex2 = Exchange("binance", service=execute_order)( Stream.source([7005, 7600, 8200], dtype="float").rename("USD-BTC"), Stream.source([201, 208, 402], dtype="float").rename("USD-ETH"), Stream.source([56, 52, 60], dtype="float").rename("USD-LTC")) wallet_btc = Wallet(ex1, 10 * BTC) wallet_btc_ds = _create_wallet_source(wallet_btc) wallet_usd = Wallet(ex2, 1000 * USD) wallet_usd.withdraw(quantity=400 * USD, reason="test") wallet_usd.deposit(quantity=Quantity(USD, 400, path_id="fake_id"), reason="test") wallet_usd_ds = _create_wallet_source(wallet_usd, include_worth=False) streams = ex1.streams() + ex2.streams() + wallet_btc_ds + wallet_usd_ds feed = DataFeed(streams) assert feed.next() == { "bitfinex:/USD-BTC": 7000, "bitfinex:/USD-ETH": 200, "bitfinex:/BTC:/free": 10, "bitfinex:/BTC:/locked": 0, "bitfinex:/BTC:/total": 10, "bitfinex:/BTC:/worth": 70000, "binance:/USD-BTC": 7005, "binance:/USD-ETH": 201, "binance:/USD-LTC": 56, "binance:/USD:/free": 600, "binance:/USD:/locked": 400, "binance:/USD:/total": 1000 }
def reset(self) -> None: """Resets the wallet.""" self.balance = Quantity(self.instrument, self._initial_size).quantize() self._locked = {}
def transfer(source: 'Wallet', target: 'Wallet', quantity: 'Quantity', commission: 'Quantity', exchange_pair: 'ExchangePair', reason: str) -> 'Transfer': """Transfers funds from one wallet to another. Parameters ---------- source : `Wallet` The wallet in which funds will be transferred from target : `Wallet` The wallet in which funds will be transferred to quantity : `Quantity` The quantity to be transferred from the source to the target. In terms of the instrument of the source wallet. commission : `Quantity` The commission to be taken from the source wallet for performing the transfer of funds. exchange_pair : `ExchangePair` The exchange pair associated with the transfer reason : str The reason for transferring the funds. Returns ------- `Transfer` A transfer object describing the transaction. Raises ------ Exception Raised if an equation that describes the conservation of funds is broken. """ quantity = quantity.quantize() commission = commission.quantize() pair = source.instrument / target.instrument poid = quantity.path_id lsb1 = source.locked.get(poid).size ltb1 = target.locked.get(poid, 0 * pair.quote).size commission = source.withdraw(commission, "COMMISSION") quantity = source.withdraw(quantity, "FILL ORDER") if quantity.instrument == exchange_pair.pair.base: instrument = exchange_pair.pair.quote converted_size = quantity.size / exchange_pair.price else: instrument = exchange_pair.pair.base converted_size = quantity.size * exchange_pair.price converted = Quantity(instrument, converted_size, quantity.path_id).quantize() converted = target.deposit( converted, 'TRADED {} {} @ {}'.format(quantity, exchange_pair, exchange_pair.price)) lsb2 = source.locked.get(poid).size ltb2 = target.locked.get(poid, 0 * pair.quote).size q = quantity.size c = commission.size cv = converted.size p = exchange_pair.inverse_price if pair == exchange_pair.pair else exchange_pair.price source_quantization = Decimal(10)**-source.instrument.precision target_quantization = Decimal(10)**-target.instrument.precision lhs = Decimal((lsb1 - lsb2) - (q + c)).quantize(source_quantization) rhs = Decimal(ltb2 - ltb1 - cv).quantize(target_quantization) lhs_eq_zero = np.isclose(float(lhs), 0, atol=float(source_quantization)) rhs_eq_zero = np.isclose(float(rhs), 0, atol=float(target_quantization)) if not lhs_eq_zero or not rhs_eq_zero: equation = "({} - {}) - ({} + {}) != ({} - {}) - {} [LHS = {}, RHS = {}, Price = {}]".format( lsb1, lsb2, q, c, ltb2, ltb1, cv, lhs, rhs, p) raise Exception("Invalid Transfer: " + equation) return Transfer(quantity, commission, exchange_pair.price)
def execute_sell_order(order: 'Order', base_wallet: 'Wallet', quote_wallet: 'Wallet', current_price: float, options: 'ExchangeOptions', clock: 'Clock') -> 'Trade': """Executes a sell order on the exchange. Parameters ---------- order : `Order` The order that is being filled. base_wallet : `Wallet` The wallet of the base instrument. quote_wallet : `Wallet` The wallet of the quote instrument. current_price : float The current price of the exchange pair. options : `ExchangeOptions` The exchange options. clock : `Clock` The clock for the trading process.. Returns ------- `Trade` The executed trade that was made. """ if order.type == TradeType.LIMIT and order.price < current_price: return None filled = order.remaining.contain(order.exchange_pair) if order.type == TradeType.MARKET: scale = order.price / max(current_price, order.price) filled = scale * filled commission = options.commission * filled quantity = filled - commission if commission.size < Decimal(10)**-quantity.instrument.precision: logging.warning( "Commission is less than instrument precision. Canceling order. " "Consider defining a custom instrument with a higher precision.") order.cancel("COMMISSION IS LESS THAN PRECISION.") return None # Todo: Fill Order with Position-Data got by Broker if quantity.instrument == order.exchange_pair.pair.base: instrument = order.exchange_pair.pair.quote converted_size = quantity.size / order.price else: instrument = order.exchange_pair.pair.base converted_size = quantity.size * order.price converted = Quantity(instrument, converted_size, quantity.path_id).quantize() position = Position( id=order.id, # to be replaced by borker-id order=order, quantity=converted, ) quote_wallet.add_position(position) transfer = Wallet.transfer(source=base_wallet, target=quote_wallet, quantity=quantity, commission=commission, exchange_pair=order.exchange_pair, reason="SELL") trade = Trade(order_id=order.id, step=clock.step, exchange_pair=order.exchange_pair, side=TradeSide.SELL, trade_type=order.type, quantity=transfer.quantity, price=transfer.price, commission=transfer.commission) return trade
def test_on_fill_with_complex_order(mock_trade_class, mock_exchange_class): exchange = mock_exchange_class.return_value exchange.options.max_trade_size = 1e6 exchange.id = "fake_exchange_id" exchange.name = "bitfinex" exchange.quote_price = lambda pair: Decimal(7000.00) broker = Broker() broker.exchanges = [exchange] wallets = [Wallet(exchange, 10000 * USD), Wallet(exchange, 0 * BTC)] portfolio = Portfolio(USD, wallets) side = TradeSide.BUY order = Order(step=0, exchange_pair=ExchangePair(exchange, USD / BTC), side=TradeSide.BUY, trade_type=TradeType.MARKET, quantity=5200.00 * USD, portfolio=portfolio, price=Decimal(7000.00)) risk_criteria = Stop("down", 0.03) ^ Stop("up", 0.02) risk_management = OrderSpec( side=TradeSide.SELL if side == TradeSide.BUY else TradeSide.BUY, trade_type=TradeType.MARKET, exchange_pair=ExchangePair(exchange, USD / BTC), criteria=risk_criteria) order.add_order_spec(risk_management) order.attach(broker) order.execute() broker.executed[order.id] = order # Execute fake trade price = Decimal(7000.00) scale = order.price / price commission = 3.00 * USD base_size = scale * order.size - commission.size trade = mock_trade_class.return_value trade.order_id = order.id trade.size = base_size trade.quantity = base_size * USD trade.price = price trade.commission = commission base_wallet = portfolio.get_wallet(exchange.id, USD) quote_wallet = portfolio.get_wallet(exchange.id, BTC) base_size = trade.size + trade.commission.size quote_size = (order.price / trade.price) * (trade.size / trade.price) base_wallet.withdraw(quantity=Quantity(USD, size=base_size, path_id=order.path_id), reason="test") quote_wallet.deposit(quantity=Quantity(BTC, size=quote_size, path_id=order.path_id), reason="test") assert trade.order_id in broker.executed.keys() assert trade not in broker.trades assert broker.unexecuted == [] order.fill(trade) assert order.remaining == 0 assert trade in broker.trades[order.id] assert broker.unexecuted != []