def test_valid_rmul(): # Quantity size = Quantity(BTC, 5) p = Price(50, USD / BTC) q = size * p assert isinstance(q, Quantity) assert q.size == 250 assert q.instrument == USD # Quantity with Path ID size = Quantity(BTC, 5, path_id="fake_id") p = Price(50, USD / BTC) q = size * p assert isinstance(q, Quantity) assert q.size == 250 assert q.instrument == USD assert q.path_id == "fake_id" # int p1 = 5 p2 = Price(50, USD / BTC) p = p1 * p2 assert p.rate == 250 assert p.pair == USD / BTC # float p1 = 5.0 p2 = Price(50, USD / BTC) p = p1 * p2 assert p.rate == 250 assert p.pair == USD / BTC
def test_valid_rtruediv(): # Quantity size = Quantity(USD, 50) p = Price(5, USD / BTC) q = size / p assert isinstance(q, Quantity) assert q.size == 10 assert q.instrument == BTC # Quantity with Path ID size = Quantity(USD, 50, "fake_id") p = Price(5, USD / BTC) q = size / p assert isinstance(q, Quantity) assert q.size == 10 assert q.instrument == BTC assert q.path_id == "fake_id" # int p1 = 50 p2 = Price(5, USD / BTC) p = p1 / p2 assert p.rate == 10 assert p.pair == BTC / USD # float p1 = 50.0 p2 = Price(5, USD / BTC) p = p1 / p2 assert p.rate == 10 assert p.pair == BTC / USD
def _execute_sell_order(self, order: 'Order', base_wallet: 'Wallet', quote_wallet: 'Wallet', current_price: float) -> Trade: price = self._contain_price(current_price) if order.type == TradeType.LIMIT and order.price > current_price: return None commission = Quantity(order.pair.base, order.size * self._commission, order.path_id) size = self._contain_size(order.size - commission.size) quantity = Quantity(order.pair.base, size, order.path_id) trade = Trade(order_id=order.id, exchange_id=self.id, step=self.clock.step, pair=order.pair, side=TradeSide.SELL, trade_type=order.type, quantity=quantity, price=price, commission=commission) # self._slippage_model.adjust_trade(trade) quote_size = trade.size / trade.price * (trade.price / order.price) quote_wallet -= Quantity(order.pair.quote, quote_size, order.path_id) base_wallet += quantity base_wallet -= commission return trade
def test_total_balance(): wallet = Wallet(exchange, 10000 * USD) wallet += Quantity(USD, 500, path_id=path_id) wallet += Quantity(USD, 700, path_id=other_id) assert wallet.total_balance == 11200 * 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 test_invalid_rtruediv(): # Quantity with instrument different than the quote p2 = Quantity(USD, 50) p1 = Price(50, BTC / USD) with pytest.raises(IncompatiblePriceQuantityOperation): p1 / p2
def test_exchange_with_wallets_feed(): ex1 = Exchange("coinbase", service=execute_order)(Array("USD-BTC", [7000, 7500, 8300]), Array("USD-ETH", [200, 212, 400])) ex2 = Exchange("binance", service=execute_order)(Array("USD-BTC", [7005, 7600, 8200]), Array("USD-ETH", [201, 208, 402]), Array("USD-LTC", [56, 52, 60])) wallet_btc = Wallet(ex1, 10 * BTC) wallet_btc_ds = create_wallet_source(wallet_btc) wallet_usd = Wallet(ex2, 1000 * USD) wallet_usd -= 400 * USD wallet_usd += Quantity(USD, 400, path_id="fake_id") wallet_usd_ds = create_wallet_source(wallet_usd, include_worth=False) feed = DataFeed([ex1, ex2, wallet_btc_ds, wallet_usd_ds]) assert feed.next() == { "coinbase:/USD-BTC": 7000, "coinbase:/USD-ETH": 200, "coinbase:/BTC:/free": 10, "coinbase:/BTC:/locked": 0, "coinbase:/BTC:/total": 10, "coinbase:/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 execute(self, exchange: 'Exchange'): self.status = OrderStatus.OPEN instrument = self.side.instrument(self.pair) wallet = self.portfolio.get_wallet(exchange.id, instrument=instrument) if self.path_id not in wallet.locked.keys(): try: wallet -= self.size * instrument except InsufficientFunds: size = wallet.balance.size wallet -= size * instrument self.quantity = Quantity(instrument, size, path_id=self.path_id) wallet += self.quantity if self.portfolio.order_listener: self.attach(self.portfolio.order_listener) for listener in self._listeners or []: listener.on_execute(self, exchange) exchange.execute_order(self, self.portfolio)
def locked_balance(self) -> 'Quantity': """The total balance of the wallet locked in orders.""" 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 += Quantity(USD, 500, path_id=path_id) wallet += Quantity(USD, 700, path_id=other_id) with pytest.raises(InsufficientFunds): wallet -= 11000 * USD with pytest.raises(InsufficientFunds): wallet -= Quantity(USD, 750, path_id) with pytest.raises(InsufficientFunds): wallet -= Quantity(USD, 750, path_id) with pytest.raises(IncompatibleInstrumentOperation): wallet -= 500 * BTC
def balance(self, instrument: Instrument) -> Quantity: """The total balance of the portfolio in a specific instrument available for use.""" balance = Quantity(instrument, 0) for (_, symbol), wallet in self._wallets.items(): if symbol == instrument.symbol: balance += wallet.balance return balance
def locked_balance(self, instrument: Instrument) -> Quantity: """The total balance of the portfolio in a specific instrument locked in orders.""" balance = Quantity(instrument, 0) for (_, symbol), wallet in self._wallets.items(): if symbol == instrument.symbol: balance += wallet.locked_balance return balance
def test_deallocate(): wallet = Wallet(exchange, 10000 * USD) wallet += Quantity(USD, 500, path_id=path_id) wallet += Quantity(USD, 700, path_id=other_id) assert wallet.balance == 10000 * USD assert wallet.locked[path_id] == 500 * USD assert wallet.locked[other_id] == 700 * USD wallet.deallocate(path_id) assert wallet.balance == 10500 * USD assert path_id not in wallet.locked.keys() assert wallet.locked[other_id] == 700 * USD wallet.deallocate(other_id) assert wallet.balance == 11200 * USD assert other_id not in wallet.locked.keys()
def _execute_buy_order(self, order: 'Order', base_wallet: 'Wallet', quote_wallet: 'Wallet', current_price: float) -> Trade: price = self._contain_price(current_price) if order.type == TradeType.LIMIT and order.price < current_price: return None commission = Quantity(order.pair.base, order.size * self._commission, order.path_id) base_size = self._contain_size(order.size - commission.size) if order.type == TradeType.MARKET: scale = order.price / price base_size = self._contain_size(scale * order.size - commission.size) base_wallet -= commission try: quantity = Quantity(order.pair.base, base_size, order.path_id) base_wallet -= quantity except InsufficientFundsForAllocation: balance = base_wallet.locked[order.path_id] quantity = Quantity(order.pair.base, balance.size, order.path_id) base_wallet -= quantity quote_size = (order.price / price) * (quantity.size / price) quote_wallet += Quantity(order.pair.quote, quote_size, order.path_id) trade = Trade(order_id=order.id, exchange_id=self.id, step=self.clock.step, pair=order.pair, side=TradeSide.BUY, trade_type=order.type, quantity=quantity, price=price, commission=commission) # self._slippage_model.adjust_trade(trade) return trade
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 execute_buy_order(order: 'Order', base_wallet: 'Wallet', quote_wallet: 'Wallet', current_price: float, options: 'ExchangeOptions', exchange_id: str, clock: 'Clock') -> 'Trade': price = contain_price(current_price, options) if order.type == TradeType.LIMIT and order.price < current_price: return None commission = Quantity(order.pair.base, order.size * options.commission, order.path_id) size = contain_size(order.size - commission.size, options) if order.type == TradeType.MARKET: scale = order.price / price size = contain_size(scale * order.size - commission.size, options) base_wallet -= commission try: quantity = Quantity(order.pair.base, size, order.path_id) base_wallet -= quantity except InsufficientFunds: balance = base_wallet.locked[order.path_id] quantity = Quantity(order.pair.base, balance.size, order.path_id) base_wallet -= quantity quote_size = (order.price / price) * (size / price) quote_wallet += Quantity(order.pair.quote, quote_size, order.path_id) trade = Trade(order_id=order.id, exchange_id=exchange_id, step=clock.step, pair=order.pair, side=TradeSide.BUY, trade_type=order.type, quantity=quantity, price=price, commission=commission) return trade
def test_valid_isub(): # Add to remove from unlocked balance wallet = Wallet(exchange, 10000 * USD) wallet -= 500 * USD assert wallet.balance == 9500 * USD assert len(wallet.locked) == 0 # Add to balance with locked path_id wallet = Wallet(exchange, 10000 * USD) wallet += Quantity(USD, 750, path_id=path_id) wallet += Quantity(USD, 1000, path_id=other_id) wallet -= Quantity(USD, 500, path_id=path_id) assert wallet.balance == 10000 * USD assert wallet.locked[path_id] == 250 * USD wallet -= Quantity(USD, 500, path_id=other_id) assert wallet.balance == 10000 * USD assert wallet.locked[other_id] == 500 * USD
def test_invalid_truediv(): # Price with different trading pairs p1 = Price(50, USD / BTC) p2 = Price(50, BTC / USD) with pytest.raises(IncompatibleTradingPairOperation): p1 / p2 # Quantity with instrument different than the quote p1 = Price(50, USD / BTC) p2 = Quantity(USD, 50) with pytest.raises(IncompatiblePriceQuantityOperation): p1 / p2
def test_valid_mul(): # Price p1 = Price(50, USD / BTC) p2 = Price(5, USD / BTC) p = p1 * p2 assert p.rate == 250 assert p.pair == USD / BTC # Quantity p = Price(50, USD / BTC) size = Quantity(BTC, 5) q = p * size assert isinstance(q, Quantity) assert q.size == 250 assert q.instrument == USD # Quantity with Path ID p = Price(50, USD / BTC) size = Quantity(BTC, 5, path_id="fake_id") q = p * size assert isinstance(q, Quantity) assert q.size == 250 assert q.instrument == USD assert q.path_id == "fake_id" # int p1 = Price(50, USD/BTC) p2 = 5 p = p1 * p2 assert p.rate == 250 assert p.pair == USD / BTC # float p1 = Price(50, USD / BTC) p2 = 5.0 p = p1 * p2 assert p.rate == 250 assert p.pair == USD / BTC
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_valid_iadd(): # Add to free unlocked balance wallet = Wallet(exchange, 10000 * USD) wallet += 500 * USD assert wallet.balance == 10500 * USD assert len(wallet.locked) == 0 # Add to balance with locked path_id wallet = Wallet(exchange, 10000 * USD) wallet += Quantity(USD, 500, path_id=path_id) assert wallet.balance == 10000 * USD assert wallet.locked[path_id] == 500 * USD # Add to more balance with locked path_id wallet += Quantity(USD, 500, path_id=path_id) assert wallet.balance == 10000 * USD assert wallet.locked[path_id] == 1000 * USD # Add to balance that has another locked path_id wallet += Quantity(USD, 500, path_id=other_id) assert wallet.balance == 10000 * USD assert wallet.locked[path_id] == 1000 * USD assert wallet.locked[other_id] == 500 * USD
def test_create_from_buy_order(mock_order_class, mock_exchange_class): exchange = mock_exchange_class.return_value exchange.id = "fake_id" wallets = [Wallet(exchange, 10000.00*USD), Wallet(exchange, 2*BTC)] portfolio = Portfolio(USD, wallets) order = mock_order_class.return_value order.portfolio = portfolio order.path_id = "fake_path_id" order.price = 7000.00 wallet_btc = portfolio.get_wallet(exchange.id, BTC) wallet_btc -= 0.4*BTC wallet_btc += Quantity(BTC, 0.4, path_id=order.path_id) assert wallet_btc.balance == 1.6 * BTC assert wallet_btc.locked[order.path_id].size == 0.4 * BTC order_spec = OrderSpec( side=TradeSide.SELL, trade_type=TradeType.MARKET, pair=USD/BTC ) next_order = order_spec.create_order(order, exchange) assert next_order assert next_order.side == TradeSide.SELL assert next_order.type == TradeType.MARKET assert next_order.pair == USD/BTC assert next_order.path_id == order.path_id assert next_order.quantity.path_id == order.path_id assert next_order.quantity.instrument == BTC
def test_create_from_sell_order(mock_order_class, mock_exchange_class): exchange = mock_exchange_class.return_value exchange.id = "fake_id" wallets = [Wallet(exchange, 10000.00*USD), Wallet(exchange, 2*BTC)] portfolio = Portfolio(USD, wallets) order = mock_order_class.return_value order.portfolio = portfolio order.path_id = "fake_path_id" order.price = 7000.00 wallet_usd = portfolio.get_wallet(exchange.id, USD) wallet_usd -= 1000*USD wallet_usd += Quantity(USD, 1000, path_id=order.path_id) assert wallet_usd.balance == 9000 * USD assert wallet_usd.locked[order.path_id].size == 1000 * USD order_spec = OrderSpec( side=TradeSide.BUY, trade_type=TradeType.MARKET, pair=USD/BTC ) next_order = order_spec.create_order(order, exchange) assert next_order assert next_order.side == TradeSide.BUY assert next_order.type == TradeType.MARKET assert next_order.pair == USD/BTC assert next_order.path_id == order.path_id assert next_order.quantity.path_id == order.path_id assert next_order.quantity.instrument == USD
def from_tuple(cls, wallet_tuple: Tuple['Exchange', 'Instrument', float]): exchange, instrument, balance = wallet_tuple return cls(exchange, Quantity(instrument, balance))
def test_on_fill_with_complex_order(mock_trade_class, mock_exchange_class): exchange = mock_exchange_class.return_value exchange.id = "fake_exchange_id" broker = Broker(exchange) wallets = [Wallet(exchange, 10000 * USD), Wallet(exchange, 0 * BTC)] portfolio = Portfolio(USD, wallets) side = TradeSide.BUY order = Order(step=0, side=TradeSide.BUY, trade_type=TradeType.MARKET, pair=USD / BTC, quantity=5200.00 * USD, portfolio=portfolio, price=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, pair=USD / BTC, criteria=risk_criteria) order += risk_management order.attach(broker) order.execute(exchange) broker._executed[order.id] = order # Execute fake trade price = 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.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 -= Quantity(USD, size=base_size, path_id=order.path_id) quote_wallet += Quantity(BTC, size=quote_size, path_id=order.path_id) assert trade.order_id in broker.executed.keys() assert trade not in broker.trades assert broker.unexecuted == [] order.fill(exchange, trade) assert order.remaining_size == 0 assert trade in broker.trades[order.id] assert broker.unexecuted != []
def execute_sell_order(order: 'Order', base_wallet: 'Wallet', quote_wallet: 'Wallet', current_price: float, options: 'ExchangeOptions', exchange_id: str, clock: 'Clock') -> 'Trade': price = contain_price(current_price, options) if order.type == TradeType.LIMIT and order.price > current_price: return None commission = Quantity(order.pair.base, order.size * options.commission, order.path_id) size = contain_size(order.size - commission.size, options) try: quote_size = (size / price) * (price / order.price) quote_quantity = Quantity(order.pair.quote, quote_size, order.path_id) quote_wallet -= quote_quantity.reason( "REMOVE FROM LOCKED TO FILL ORDER") except InsufficientFunds: balance = quote_wallet.locked[order.path_id] quote_size = balance.size remove_quantity = Quantity(order.pair.quote, quote_size, order.path_id) quote_wallet -= remove_quantity.reason( "REMOVE FROM LOCKED TO FILL ORDER (INSUFFICIENT FUNDS)") base_size = (quote_size * price) / (price / order.price) quantity = Quantity(order.pair.base, base_size, order.path_id) base_wallet += quantity.reason("SOLD @ {} {}".format(price, order.pair)) base_wallet -= commission.reason("COMMISSION FOR SELL") trade = Trade(order_id=order.id, exchange_id=exchange_id, step=clock.step, pair=order.pair, side=TradeSide.SELL, trade_type=order.type, quantity=quantity, price=price, commission=commission) return trade
def reset(self): self._balance = Quantity(self._instrument, self._initial_size) self._locked = {}
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 = "coinbase" 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 += 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 != []
def test_invalid_rmul(): # Quantity with instrument different than the quote p1 = Quantity(USD, 50) p2 = Price(50, USD / BTC) with pytest.raises(IncompatiblePriceQuantityOperation): p1 * p2
def transfer(source: 'Wallet', target: 'Wallet', quantity: 'Quantity', commission: 'Quantity', exchange_pair: 'ExchangePair', reason: str): """ 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 """ 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)