Module xchg.xchg

Simulator of a currency exchange.

Expand source code
'''Simulator of a currency exchange.'''

from .common import _read_candles


class Xchg:
    def __init__(self, fee, min_order_size, data_path=None, balance=None,
                 candles=None):
        '''Create an instance of a currency exchange.

        Args:
          fee: What part of a trade volume will be paid as fee. For example, 1%
              fee should be set as 0.01.
          min_order_size: Minimum trade volume expressed in a base currency
              (cash).
          data_path: Where csv files with data are stored.
          balance: An initial balance. If it's None, then cash currency will be
              set to 1.0 and all others to zero. If it's a float, then it will
              set as the base currency (cash) value. Also you set it as a full
              dictionary with all currencies as keys and values.
          candles: You can directly initialize the class with candles, not to
              read them from a disk.
        '''
        self.__fee = fee
        self.__min_order_size = min_order_size

        if data_path is not None:
            res = _read_candles(data_path)
            self.__currencies = res['currencies']
            self.__candles = res['candles']
        else:
            self.__currencies = list(sorted(candles[0].keys()))
            self.__candles = candles

        self.__data_start = self.__candles[0][self.__currencies[0]]['date']
        self.__data_end = self.__candles[-1][self.__currencies[0]]['date']

        if type(balance) == dict:
            self.__balance = {}
            for currency in ['cash'] + self.__currencies:
                self.__balance[currency] = float(balance.get(currency, 0.0))
        elif balance is None:
            self.__balance = {}
            self.__balance['cash'] = 1.0
            for currency in self.__currencies:
                self.__balance[currency] = 0.0
        else:
            self.__balance = {}
            self.__balance['cash'] = float(balance)
            for currency in self.__currencies:
                self.__balance[currency] = 0.0

    def __repr__(self):
        '''Returns class attributes as a string.'''
        return (f"balance: {self.balance}\n"
                f"fee: {self.fee}\n"
                f"min_order_size: {self.min_order_size}\n"
                f"currencies: {self.currencies}\n"
                f"current_candle: {self.current_candle}\n"
                f"capital: {self.capital}\n"
                f"portfolio: {self.portfolio}\n"
                f"lenght: {len(self)}\n"
                f"data_start: {self.data_start}\n"
                f"data_end: {self.data_end}")

    def __len__(self):
        '''Returns the number of candles.'''
        return len(self.__candles)

    @property
    def data_start(self) -> int:
        '''Get a starting time of the data.

        Returns:
            A starting time of the data.
        '''
        return self.__data_start

    @property
    def data_end(self) -> int:
        '''Get an ending time of the data.

        Returns:
            An ending time of the data.
        '''
        return self.__data_end

    @property
    def current_candle(self) -> dict:
        '''Get a current candle.

        Returns:
            A current candle.
        '''
        return self.__candles[0]

    @property
    def balance(self) -> float:
        '''Get a current balance.

        Returns:
            A current balance.
        '''
        return self.__balance

    @property
    def fee(self) -> float:
        '''Get a fee which is used in the exchange.

        Returns:
            A fee.
        '''
        return self.__fee

    @property
    def currencies(self) -> float:
        '''Get currencies which are used in the exchange.

        Returns:
            List of currencies.
        '''
        return self.__currencies

    @property
    def min_order_size(self) -> float:
        '''Get a minimum order size which is set in the exchange.

        Returns:
            A minimum order value expressed in a cash currency.
        '''
        return self.__min_order_size

    @property
    def capital(self) -> float:
        '''Returns a current capital - sum of all currencies if they are
        converted to a cash without fees.

        Returns:
            A capital.
        '''
        capital = 0
        for currency, amount in self.balance.items():
            if currency == 'cash':
                capital += amount
            else:
                capital += amount * self.current_candle[currency]['close']
        return capital

    @property
    def portfolio(self) -> dict:
        '''Returns a current portfolio - proportion of capital by each
        currency.

        Returns:
            A portfolio.
        '''
        cap = self.capital
        portf = {}
        for currency, amount in self.balance.items():
            if currency == 'cash':
                portf[currency] = self.balance[currency] / cap
            else:
                portf[currency] = (self.balance[currency]
                                   * self.current_candle[currency]['close']
                                   / cap)
        return portf

    def next_step(self) -> 'Xchg':
        '''Go to the next step in timeline.

        Returns:
            A new Xchg instance with one candle removed.
        '''
        if len(self.__candles) == 1:
            raise StopIteration
        return Xchg(self.fee, self.min_order_size, balance=self.balance,
                    candles=self.__candles[1:])

    def buy(self, currency: str, amount: float) -> dict:
        '''Buy currency.

        Args:
            currency: A name of the currency.
            amount: How much units of this currency to buy.

        Returns:
            A new Xchg instance after a buy operation.
        '''

        balance = self.balance.copy()
        price = self.current_candle[currency]['close']
        currency_delta = amount * (1 - self.fee)
        cash_delta = price * amount

        # If we want to buy a slightly more than we have, we will forgive.
        if (cash_delta <= (self.balance['cash'] + 1e-10)
                and cash_delta >= self.min_order_size):
            balance['cash'] -= cash_delta
            balance[currency] += currency_delta
            if balance['cash'] < 0.0:
                # Set the balance to zero if we bought a slightly more than we
                # have.
                balance['cash'] = 0.0

        return Xchg(self.fee, self.min_order_size, balance=balance,
                    candles=self.__candles)

    def sell(self, currency: str, amount: float) -> dict:
        '''Sell currency.

        Args:
            currency: A name of the currency.
            amount: How much units of this currency to sell.

        Returns:
            A new Xchg instance after a sell operation.
        '''

        balance = self.balance.copy()
        price = self.current_candle[currency]['close']
        without_fee = price * amount
        with_fee = without_fee * (1 - self.fee)

        # If we want to sell a slightly more than we have, we will forgive.
        if (amount <= (self.balance[currency] + 1e-10)
                and without_fee >= self.min_order_size):
            balance['cash'] += with_fee
            balance[currency] -= amount
            if balance[currency] < 0.0:
                # Set the balance to zero if we bought a slightly more than we
                # have.
                balance[currency] = 0.0

        return Xchg(self.fee, self.min_order_size, balance=balance,
                    candles=self.__candles)

    def make_portfolio(self, target_portfolio: dict) -> dict:
        '''Make a desired portfolio.

        Args:
            target_portfolio: A desired portfolio.

        Returns:
            A new Xchg instance with a desired portfolio distribution.
        '''

        x = self

        # Calculate a capital change after trading.
        cc0 = 1
        cc1 = 1 - 2 * x.fee + x.fee ** 2
        while abs(cc1 - cc0) > 1e-10:
            cc0 = cc1

            # How much we will get from selling.
            sell_amount = 0
            for cur in x.currencies:
                amount = x.portfolio[cur] - cc1 * target_portfolio[cur]
                if amount > 0:
                    sell_amount += amount

            cc1 = (1 - x.fee * x.portfolio['cash'] - (2 * x.fee - x.fee ** 2)
                   * sell_amount) \
                / (1 - x.fee * target_portfolio['cash'])

        # A capital after trade.
        tar_capital = x.capital * cc1

        # Sell first.
        for cur in x.currencies:
            amount = tar_capital * target_portfolio[cur] \
                     / x.current_candle[cur]['close'] - x.balance[cur]
            if amount < 0:
                x = x.sell(cur, abs(amount))

        # Then buy.
        for cur in x.currencies:
            amount = tar_capital * target_portfolio[cur] \
                     / x.current_candle[cur]['close'] - x.balance[cur]
            if amount > 0:
                x = x.buy(cur, abs(amount / (1 - self.fee)))

        return x

Classes

class Xchg (fee, min_order_size, data_path=None, balance=None, candles=None)

Create an instance of a currency exchange.

Args

fee
What part of a trade volume will be paid as fee. For example, 1% fee should be set as 0.01.
min_order_size
Minimum trade volume expressed in a base currency (cash).
data_path
Where csv files with data are stored.
balance
An initial balance. If it's None, then cash currency will be set to 1.0 and all others to zero. If it's a float, then it will set as the base currency (cash) value. Also you set it as a full dictionary with all currencies as keys and values.
candles
You can directly initialize the class with candles, not to read them from a disk.
Expand source code
class Xchg:
    def __init__(self, fee, min_order_size, data_path=None, balance=None,
                 candles=None):
        '''Create an instance of a currency exchange.

        Args:
          fee: What part of a trade volume will be paid as fee. For example, 1%
              fee should be set as 0.01.
          min_order_size: Minimum trade volume expressed in a base currency
              (cash).
          data_path: Where csv files with data are stored.
          balance: An initial balance. If it's None, then cash currency will be
              set to 1.0 and all others to zero. If it's a float, then it will
              set as the base currency (cash) value. Also you set it as a full
              dictionary with all currencies as keys and values.
          candles: You can directly initialize the class with candles, not to
              read them from a disk.
        '''
        self.__fee = fee
        self.__min_order_size = min_order_size

        if data_path is not None:
            res = _read_candles(data_path)
            self.__currencies = res['currencies']
            self.__candles = res['candles']
        else:
            self.__currencies = list(sorted(candles[0].keys()))
            self.__candles = candles

        self.__data_start = self.__candles[0][self.__currencies[0]]['date']
        self.__data_end = self.__candles[-1][self.__currencies[0]]['date']

        if type(balance) == dict:
            self.__balance = {}
            for currency in ['cash'] + self.__currencies:
                self.__balance[currency] = float(balance.get(currency, 0.0))
        elif balance is None:
            self.__balance = {}
            self.__balance['cash'] = 1.0
            for currency in self.__currencies:
                self.__balance[currency] = 0.0
        else:
            self.__balance = {}
            self.__balance['cash'] = float(balance)
            for currency in self.__currencies:
                self.__balance[currency] = 0.0

    def __repr__(self):
        '''Returns class attributes as a string.'''
        return (f"balance: {self.balance}\n"
                f"fee: {self.fee}\n"
                f"min_order_size: {self.min_order_size}\n"
                f"currencies: {self.currencies}\n"
                f"current_candle: {self.current_candle}\n"
                f"capital: {self.capital}\n"
                f"portfolio: {self.portfolio}\n"
                f"lenght: {len(self)}\n"
                f"data_start: {self.data_start}\n"
                f"data_end: {self.data_end}")

    def __len__(self):
        '''Returns the number of candles.'''
        return len(self.__candles)

    @property
    def data_start(self) -> int:
        '''Get a starting time of the data.

        Returns:
            A starting time of the data.
        '''
        return self.__data_start

    @property
    def data_end(self) -> int:
        '''Get an ending time of the data.

        Returns:
            An ending time of the data.
        '''
        return self.__data_end

    @property
    def current_candle(self) -> dict:
        '''Get a current candle.

        Returns:
            A current candle.
        '''
        return self.__candles[0]

    @property
    def balance(self) -> float:
        '''Get a current balance.

        Returns:
            A current balance.
        '''
        return self.__balance

    @property
    def fee(self) -> float:
        '''Get a fee which is used in the exchange.

        Returns:
            A fee.
        '''
        return self.__fee

    @property
    def currencies(self) -> float:
        '''Get currencies which are used in the exchange.

        Returns:
            List of currencies.
        '''
        return self.__currencies

    @property
    def min_order_size(self) -> float:
        '''Get a minimum order size which is set in the exchange.

        Returns:
            A minimum order value expressed in a cash currency.
        '''
        return self.__min_order_size

    @property
    def capital(self) -> float:
        '''Returns a current capital - sum of all currencies if they are
        converted to a cash without fees.

        Returns:
            A capital.
        '''
        capital = 0
        for currency, amount in self.balance.items():
            if currency == 'cash':
                capital += amount
            else:
                capital += amount * self.current_candle[currency]['close']
        return capital

    @property
    def portfolio(self) -> dict:
        '''Returns a current portfolio - proportion of capital by each
        currency.

        Returns:
            A portfolio.
        '''
        cap = self.capital
        portf = {}
        for currency, amount in self.balance.items():
            if currency == 'cash':
                portf[currency] = self.balance[currency] / cap
            else:
                portf[currency] = (self.balance[currency]
                                   * self.current_candle[currency]['close']
                                   / cap)
        return portf

    def next_step(self) -> 'Xchg':
        '''Go to the next step in timeline.

        Returns:
            A new Xchg instance with one candle removed.
        '''
        if len(self.__candles) == 1:
            raise StopIteration
        return Xchg(self.fee, self.min_order_size, balance=self.balance,
                    candles=self.__candles[1:])

    def buy(self, currency: str, amount: float) -> dict:
        '''Buy currency.

        Args:
            currency: A name of the currency.
            amount: How much units of this currency to buy.

        Returns:
            A new Xchg instance after a buy operation.
        '''

        balance = self.balance.copy()
        price = self.current_candle[currency]['close']
        currency_delta = amount * (1 - self.fee)
        cash_delta = price * amount

        # If we want to buy a slightly more than we have, we will forgive.
        if (cash_delta <= (self.balance['cash'] + 1e-10)
                and cash_delta >= self.min_order_size):
            balance['cash'] -= cash_delta
            balance[currency] += currency_delta
            if balance['cash'] < 0.0:
                # Set the balance to zero if we bought a slightly more than we
                # have.
                balance['cash'] = 0.0

        return Xchg(self.fee, self.min_order_size, balance=balance,
                    candles=self.__candles)

    def sell(self, currency: str, amount: float) -> dict:
        '''Sell currency.

        Args:
            currency: A name of the currency.
            amount: How much units of this currency to sell.

        Returns:
            A new Xchg instance after a sell operation.
        '''

        balance = self.balance.copy()
        price = self.current_candle[currency]['close']
        without_fee = price * amount
        with_fee = without_fee * (1 - self.fee)

        # If we want to sell a slightly more than we have, we will forgive.
        if (amount <= (self.balance[currency] + 1e-10)
                and without_fee >= self.min_order_size):
            balance['cash'] += with_fee
            balance[currency] -= amount
            if balance[currency] < 0.0:
                # Set the balance to zero if we bought a slightly more than we
                # have.
                balance[currency] = 0.0

        return Xchg(self.fee, self.min_order_size, balance=balance,
                    candles=self.__candles)

    def make_portfolio(self, target_portfolio: dict) -> dict:
        '''Make a desired portfolio.

        Args:
            target_portfolio: A desired portfolio.

        Returns:
            A new Xchg instance with a desired portfolio distribution.
        '''

        x = self

        # Calculate a capital change after trading.
        cc0 = 1
        cc1 = 1 - 2 * x.fee + x.fee ** 2
        while abs(cc1 - cc0) > 1e-10:
            cc0 = cc1

            # How much we will get from selling.
            sell_amount = 0
            for cur in x.currencies:
                amount = x.portfolio[cur] - cc1 * target_portfolio[cur]
                if amount > 0:
                    sell_amount += amount

            cc1 = (1 - x.fee * x.portfolio['cash'] - (2 * x.fee - x.fee ** 2)
                   * sell_amount) \
                / (1 - x.fee * target_portfolio['cash'])

        # A capital after trade.
        tar_capital = x.capital * cc1

        # Sell first.
        for cur in x.currencies:
            amount = tar_capital * target_portfolio[cur] \
                     / x.current_candle[cur]['close'] - x.balance[cur]
            if amount < 0:
                x = x.sell(cur, abs(amount))

        # Then buy.
        for cur in x.currencies:
            amount = tar_capital * target_portfolio[cur] \
                     / x.current_candle[cur]['close'] - x.balance[cur]
            if amount > 0:
                x = x.buy(cur, abs(amount / (1 - self.fee)))

        return x

Instance variables

var balance : float

Get a current balance.

Returns

A current balance.

Expand source code
@property
def balance(self) -> float:
    '''Get a current balance.

    Returns:
        A current balance.
    '''
    return self.__balance
var capital : float

Returns a current capital - sum of all currencies if they are converted to a cash without fees.

Returns

A capital.

Expand source code
@property
def capital(self) -> float:
    '''Returns a current capital - sum of all currencies if they are
    converted to a cash without fees.

    Returns:
        A capital.
    '''
    capital = 0
    for currency, amount in self.balance.items():
        if currency == 'cash':
            capital += amount
        else:
            capital += amount * self.current_candle[currency]['close']
    return capital
var currencies : float

Get currencies which are used in the exchange.

Returns

List of currencies.

Expand source code
@property
def currencies(self) -> float:
    '''Get currencies which are used in the exchange.

    Returns:
        List of currencies.
    '''
    return self.__currencies
var current_candle : dict

Get a current candle.

Returns

A current candle.

Expand source code
@property
def current_candle(self) -> dict:
    '''Get a current candle.

    Returns:
        A current candle.
    '''
    return self.__candles[0]
var data_end : int

Get an ending time of the data.

Returns

An ending time of the data.

Expand source code
@property
def data_end(self) -> int:
    '''Get an ending time of the data.

    Returns:
        An ending time of the data.
    '''
    return self.__data_end
var data_start : int

Get a starting time of the data.

Returns

A starting time of the data.

Expand source code
@property
def data_start(self) -> int:
    '''Get a starting time of the data.

    Returns:
        A starting time of the data.
    '''
    return self.__data_start
var fee : float

Get a fee which is used in the exchange.

Returns

A fee.

Expand source code
@property
def fee(self) -> float:
    '''Get a fee which is used in the exchange.

    Returns:
        A fee.
    '''
    return self.__fee
var min_order_size : float

Get a minimum order size which is set in the exchange.

Returns

A minimum order value expressed in a cash currency.

Expand source code
@property
def min_order_size(self) -> float:
    '''Get a minimum order size which is set in the exchange.

    Returns:
        A minimum order value expressed in a cash currency.
    '''
    return self.__min_order_size
var portfolio : dict

Returns a current portfolio - proportion of capital by each currency.

Returns

A portfolio.

Expand source code
@property
def portfolio(self) -> dict:
    '''Returns a current portfolio - proportion of capital by each
    currency.

    Returns:
        A portfolio.
    '''
    cap = self.capital
    portf = {}
    for currency, amount in self.balance.items():
        if currency == 'cash':
            portf[currency] = self.balance[currency] / cap
        else:
            portf[currency] = (self.balance[currency]
                               * self.current_candle[currency]['close']
                               / cap)
    return portf

Methods

def buy(self, currency: str, amount: float) ‑> dict

Buy currency.

Args

currency
A name of the currency.
amount
How much units of this currency to buy.

Returns

A new Xchg instance after a buy operation.

Expand source code
def buy(self, currency: str, amount: float) -> dict:
    '''Buy currency.

    Args:
        currency: A name of the currency.
        amount: How much units of this currency to buy.

    Returns:
        A new Xchg instance after a buy operation.
    '''

    balance = self.balance.copy()
    price = self.current_candle[currency]['close']
    currency_delta = amount * (1 - self.fee)
    cash_delta = price * amount

    # If we want to buy a slightly more than we have, we will forgive.
    if (cash_delta <= (self.balance['cash'] + 1e-10)
            and cash_delta >= self.min_order_size):
        balance['cash'] -= cash_delta
        balance[currency] += currency_delta
        if balance['cash'] < 0.0:
            # Set the balance to zero if we bought a slightly more than we
            # have.
            balance['cash'] = 0.0

    return Xchg(self.fee, self.min_order_size, balance=balance,
                candles=self.__candles)
def make_portfolio(self, target_portfolio: dict) ‑> dict

Make a desired portfolio.

Args

target_portfolio
A desired portfolio.

Returns

A new Xchg instance with a desired portfolio distribution.

Expand source code
def make_portfolio(self, target_portfolio: dict) -> dict:
    '''Make a desired portfolio.

    Args:
        target_portfolio: A desired portfolio.

    Returns:
        A new Xchg instance with a desired portfolio distribution.
    '''

    x = self

    # Calculate a capital change after trading.
    cc0 = 1
    cc1 = 1 - 2 * x.fee + x.fee ** 2
    while abs(cc1 - cc0) > 1e-10:
        cc0 = cc1

        # How much we will get from selling.
        sell_amount = 0
        for cur in x.currencies:
            amount = x.portfolio[cur] - cc1 * target_portfolio[cur]
            if amount > 0:
                sell_amount += amount

        cc1 = (1 - x.fee * x.portfolio['cash'] - (2 * x.fee - x.fee ** 2)
               * sell_amount) \
            / (1 - x.fee * target_portfolio['cash'])

    # A capital after trade.
    tar_capital = x.capital * cc1

    # Sell first.
    for cur in x.currencies:
        amount = tar_capital * target_portfolio[cur] \
                 / x.current_candle[cur]['close'] - x.balance[cur]
        if amount < 0:
            x = x.sell(cur, abs(amount))

    # Then buy.
    for cur in x.currencies:
        amount = tar_capital * target_portfolio[cur] \
                 / x.current_candle[cur]['close'] - x.balance[cur]
        if amount > 0:
            x = x.buy(cur, abs(amount / (1 - self.fee)))

    return x
def next_step(self) ‑> Xchg

Go to the next step in timeline.

Returns

A new Xchg instance with one candle removed.

Expand source code
def next_step(self) -> 'Xchg':
    '''Go to the next step in timeline.

    Returns:
        A new Xchg instance with one candle removed.
    '''
    if len(self.__candles) == 1:
        raise StopIteration
    return Xchg(self.fee, self.min_order_size, balance=self.balance,
                candles=self.__candles[1:])
def sell(self, currency: str, amount: float) ‑> dict

Sell currency.

Args

currency
A name of the currency.
amount
How much units of this currency to sell.

Returns

A new Xchg instance after a sell operation.

Expand source code
def sell(self, currency: str, amount: float) -> dict:
    '''Sell currency.

    Args:
        currency: A name of the currency.
        amount: How much units of this currency to sell.

    Returns:
        A new Xchg instance after a sell operation.
    '''

    balance = self.balance.copy()
    price = self.current_candle[currency]['close']
    without_fee = price * amount
    with_fee = without_fee * (1 - self.fee)

    # If we want to sell a slightly more than we have, we will forgive.
    if (amount <= (self.balance[currency] + 1e-10)
            and without_fee >= self.min_order_size):
        balance['cash'] += with_fee
        balance[currency] -= amount
        if balance[currency] < 0.0:
            # Set the balance to zero if we bought a slightly more than we
            # have.
            balance[currency] = 0.0

    return Xchg(self.fee, self.min_order_size, balance=balance,
                candles=self.__candles)