Source code for tensortrade.oms.wallets.wallet

# Copyright 2020 The TensorTrade Authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License

from typing import Dict, Tuple
from collections import namedtuple
from decimal import Decimal

import numpy as np

from tensortrade.core import Identifiable
from tensortrade.core.exceptions import (
    InsufficientFunds,
    DoubleLockedQuantity,
    DoubleUnlockedQuantity,
    QuantityNotLocked
)
from tensortrade.oms.instruments import Instrument, Quantity, ExchangePair
from tensortrade.oms.orders import Order
from tensortrade.oms.exchanges import Exchange
from tensortrade.oms.wallets.ledger import Ledger


Transfer = namedtuple("Transfer", ["quantity", "commission", "price"])


[docs]class Wallet(Identifiable): """A wallet stores the balance of a specific instrument on a specific exchange. Parameters ---------- exchange : `Exchange` The exchange associated with this wallet. balance : `Quantity` The initial balance quantity for the wallet. """ ledger = Ledger() def __init__(self, exchange: 'Exchange', balance: 'Quantity'): self.exchange = exchange self._initial_size = balance.size self.instrument = balance.instrument self.balance = balance.quantize() self._locked = {} @property 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 @property def total_balance(self) -> 'Quantity': """The total balance of the wallet available for use and locked in orders. (`Quantity`, read-only)""" total_balance = self.balance for quantity in self._locked.values(): total_balance += quantity.size return total_balance @property def locked(self) -> 'Dict[str, Quantity]': """The current quantities that are locked for orders. (`Dict[str, Quantity]`, read-only)""" return self._locked
[docs] def lock(self, quantity, order: 'Order', reason: str) -> 'Quantity': """Locks funds for specified order. Parameters ---------- quantity : `Quantity` The amount of funds to lock for the order. order : `Order` The order funds will be locked for. reason : str The reason for locking funds. Returns ------- `Quantity` The locked quantity for `order`. Raises ------ DoubleLockedQuantity Raised if the given amount is already a locked quantity. InsufficientFunds Raised if amount is greater the current balance. """ if quantity.is_locked: raise DoubleLockedQuantity(quantity) if quantity > self.balance: if (quantity-self.balance)>Decimal(10)**(-self.instrument.precision+2): raise InsufficientFunds(self.balance, quantity) else: quantity = self.balance self.balance -= quantity quantity = quantity.lock_for(order.path_id) if quantity.path_id not in self._locked: self._locked[quantity.path_id] = quantity else: self._locked[quantity.path_id] += quantity self._locked[quantity.path_id] = self._locked[quantity.path_id].quantize() self.balance = self.balance.quantize() self.ledger.commit(wallet=self, quantity=quantity, source="{}:{}/free".format(self.exchange.name, self.instrument), target="{}:{}/locked".format(self.exchange.name, self.instrument), memo="LOCK ({})".format(reason)) return quantity
[docs] def unlock(self, quantity: 'Quantity', reason: str) -> 'Quantity': """Unlocks a certain amount from the locked funds of the wallet that are associated with the given `quantity` path id. Parameters ---------- quantity : `Quantity` The quantity to unlock from the funds. reason : str The reason for unlocking funds. Returns ------- `Quantity` The free quantity. Raises ------ DoubleUnlockedFunds Raised if `quantity` is not a locked quantity. QuantityNotLocked Raised if `quantity` has a path id that is not currently allocated in this wallet. InsufficientFunds Raised if `quantity` is greater than the amount currently allocated for the associated path id. """ if not quantity.is_locked: raise DoubleUnlockedQuantity(quantity) if quantity.path_id not in self._locked: raise QuantityNotLocked(quantity) if quantity > self._locked[quantity.path_id]: raise InsufficientFunds(self._locked[quantity.path_id], quantity) self._locked[quantity.path_id] -= quantity self.balance += quantity.free() self._locked[quantity.path_id] = self._locked[quantity.path_id].quantize() self.balance = self.balance.quantize() self.ledger.commit(wallet=self, quantity=quantity, source="{}:{}/locked".format(self.exchange.name, self.instrument), target="{}:{}/free".format(self.exchange.name, self.instrument), memo="UNLOCK {} ({})".format(self.instrument, reason)) return quantity
[docs] def deposit(self, quantity: 'Quantity', reason: str) -> 'Quantity': """Deposits funds into the wallet. Parameters ---------- quantity : `Quantity` The amount to deposit into this wallet. reason : str The reason for depositing the amount. Returns ------- `Quantity` The deposited amount. """ if quantity.is_locked: if quantity.path_id not in self._locked: self._locked[quantity.path_id] = quantity else: self._locked[quantity.path_id] += quantity else: self.balance += quantity self.balance = self.balance.quantize() self.ledger.commit(wallet=self, quantity=quantity, source=self.exchange.name, target="{}:{}/locked".format(self.exchange.name, self.instrument), memo="DEPOSIT ({})".format(reason)) return quantity
[docs] def withdraw(self, quantity: 'Quantity', reason: str) -> 'Quantity': """Withdraws funds from the wallet. Parameters ---------- quantity : `Quantity` The amount to withdraw from this wallet. reason : str The reason for withdrawing the amount. Returns ------- `Quantity` The withdrawn amount. """ if quantity.is_locked and self._locked.get(quantity.path_id, False): locked_quantity = self._locked[quantity.path_id] if quantity > locked_quantity: if (quantity-locked_quantity)>Decimal(10)**(-self.instrument.precision+2): raise InsufficientFunds(locked_quantity, quantity) else: quantity = locked_quantity self._locked[quantity.path_id] -= quantity elif not quantity.is_locked: if quantity > self.balance: if (quantity-self.balance)>Decimal(10)**(-self.instrument.precision+2): raise InsufficientFunds(self.balance, quantity) else: quantity = self.balance self.balance -= quantity self.balance = self.balance.quantize() self.ledger.commit(wallet=self, quantity=quantity, source="{}:{}/locked".format(self.exchange.name, self.instrument), target=self.exchange.name, memo="WITHDRAWAL ({})".format(reason)) return quantity
[docs] @classmethod 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))
[docs] @staticmethod 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 if commission.as_float() > 0.0: 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)
[docs] def reset(self) -> None: """Resets the wallet.""" self.balance = Quantity(self.instrument, self._initial_size).quantize() self._locked = {}
def __str__(self) -> str: return '<Wallet: balance={}, locked={}>'.format(self.balance, self.locked_balance) def __repr__(self) -> str: return str(self)