Module finlab_crypto.online

Expand source code
import sys
import time
import plotly.express as px
import pandas as pd
import datetime
from IPython.display import display
from binance.enums import *
from finlab_crypto.crawler import get_nbars_binance, get_all_binance
from binance.client import Client


class TickerInfo():
    """Ticker basic info.

    Get asset amount and convert price to BTC .

    Attributes:
        client: A Binance client object where api_key, api_secret is required.

    """
    def __init__(self, client):
        self.exinfo = client.get_exchange_info()
        self.info = client.get_account()
        self.tickers = client.get_symbol_ticker()

    @staticmethod
    def _list_select(list, key, value):
        ret = [l for l in list if l[key] == value]
        if len(ret) == 0:
            return None
        else:
            return ret[0]

    def get_base_asset(self, symbol):
        """Get base asset data of a given symbol.

        Args:
          symbol: A str of trading target name.

        Returns:
            A str of base asset (ex: 'BTC').
        """
        sinfo = self._list_select(self.exinfo['symbols'], 'symbol', symbol)
        return sinfo['baseAsset']

    def get_quote_asset(self, symbol):
        """Get quote asset data of a given symbol.

        Args:
          symbol: A str of trading target name.

        Returns:
          A float of quote asset.
        """
        sinfo = self._list_select(self.exinfo['symbols'], 'symbol', symbol)
        return sinfo['quoteAsset']

    def get_asset_price_in_btc(self, asset):
        """Convert price to BTC .

        Args:
          asset: A str of asset name (ex: 'ETH').

        Returns:
          A float of price in BTC.
        """
        if asset == 'BTC':
            return 1

        ret = self._list_select(self.tickers, 'symbol', asset + 'BTC')

        if ret is not None:
            return float(ret['price'])

        ret = self._list_select(self.tickers, 'symbol', 'BTC' + asset)
        if ret is not None:
            return 1 / float(ret['price'])

        return None


class TradingMethod():
    """Trading method in online init setting.

    Create trading method object for TradingPortfolio register .

    Attributes:
        symbols: A list of trading pair (ex: ['USDTBTC','ETHBTC']).
        freq: A str of trading time period (ex: '4h').
        lookback: An int of the length of historical data (ex:1000).
        strategy: A function that is your customized strategy (ex:trend_strategy).
        variables: A dict of your customized strategy attributes (ex:dict(name='sma',n1=30,n2=130,),).
        weight_btc: A float of btc for each commodity operation (ex: 0.2).
        filters: A dict that is your customized filter (ex:{}).
        name: A str of your trading method name (ex:'altcoin-trend-hullma').

    """
    def __init__(self, symbols, freq, lookback, strategy, variables, weight_btc, filters=None, name=''):
        self.symbols = symbols
        self.freq = freq
        self.lookback = lookback
        self.strategy = strategy
        self.variables = variables
        self.weight_btc = weight_btc
        self.filters = filters
        self.name = name


class TradingPortfolio():
    """Connect Binance account.

    The core class to connect Binance  with API, in order to connect account info,
    register strategt.

    Attributes:
        binance_key: A str of is binance authorization key.
        binance_secret: A str of is binance authorization secret.

    """
    def __init__(self, binance_key, binance_secret):
        self._client = Client(api_key=binance_key, api_secret=binance_secret)
        self._trading_methods = []
        self._margins = {}
        self.ticker_info = TickerInfo(self._client)
        self.quote_asset = 'BTC'
        self.default_stable_coin = 'USDT'

    def set_default_stable_coin(self, token):
        self.default_stable_coin = token

    def register(self, trading_method):
        """Rigister TradingMethod object.
        Args:
          trading_method: A object of TradingMethod().
        """
        self._trading_methods.append(trading_method)

    def register_margin(self, asset, weight_btc):
        """Rigister weight_btc as operation amount.
        Args:
          asset: A str of asset name (ex: 'USDT')
          weight_btc: A float of btc for each commodity operation (ex: 0.2)
        """
        self._margins[asset] = weight_btc

    def get_all_symbol_lookback(self):
        """Get all symbol lookback.
        Use in get_ohlcvs(self) function.
        Returns:
            A dict of OHLCV lookback.

        """
        symbol_lookbacks = {}
        for method in self._trading_methods:
            for a in method.symbols:
                if (a, method.freq) not in symbol_lookbacks or method.lookback > symbol_lookbacks[(a, method.freq)]:
                    symbol_lookbacks[(a, method.freq)] = method.lookback

        # add quote asset historical data
        addition = {}
        for (symbol, freq), lookback in symbol_lookbacks.items():
            base_asset = self.ticker_info.get_base_asset(symbol)
            if base_asset != self.quote_asset:
                new_symbol = base_asset + self.quote_asset
                addition[(new_symbol, freq)] = lookback

        return {**symbol_lookbacks, **addition}

    def get_ohlcvs(self):
        """Getting histrical price data through binance api.

        Returns:
            A DataFrame of OHLCV data , the number of data length is lookback.

        """
        symbol_lookbacks = self.get_all_symbol_lookback()

        ohlcvs = {}
        for (symbol, freq), lookback in symbol_lookbacks.items():
            ohlcvs[(symbol, freq)] = get_nbars_binance(symbol, freq, lookback, self._client)

        return ohlcvs

    def get_full_ohlcvs(self):
        """Getting all histrical price data through binance api.

        Returns:
            A DataFrame of OHLCV data for all.

        """
        symbol_lookbacks = self.get_all_symbol_lookback()
        ohlcvs = {}

        for (symbol, freq), lookback in symbol_lookbacks.items():
            ohlcvs[(symbol, freq)] = get_all_binance(symbol, freq)
            time.sleep(3)
        return ohlcvs

    def get_latest_signals(self, ohlcvs, html=False):
        """Get latest signals dataframe.

        Choose which strategy to implement on widgets GUI.

        Args:
          ohlcvs: A dataframe of symbel.
          html: A bool of controlling html generation.

        Returns:
          A dataframe of latest_signals data,
          The last_signals column is bool value of whether to execute the transaction.
          The value_in_btc column is present value of assets.
        """
        ret = []
        for method in self._trading_methods:
            for symbol in method.symbols:
                ohlcv = ohlcvs[(symbol, method.freq)]
                htmlname = f'{symbol}-{method.freq}-{method.name}.html' if html else None
                result = method.strategy.backtest(ohlcv,
                                                  method.variables, filters=method.filters, plot=html,
                                                  html=htmlname,
                                                  freq=method.freq, fees=0., slippage=0.)

                signal = result.cash().iloc[-1] == 0
                return_ = 0

                # find weight_btc if it is in the nested dictionary
                weight_btc = method.weight_btc
                if isinstance(weight_btc, dict):
                    weight_btc = (weight_btc[symbol]
                        if symbol in weight_btc else weight_btc['default'])

                entry_price = 0
                entry_time = 0
                value_in_btc = 0
                if signal:
                    txn = result.positions().records
                    rds = result.orders().records
                    return_ = ohlcv.close.iloc[-1] / rds['price'].iloc[-1] - 1
                    entry_price = rds['price'].iloc[-1]
                    entry_time = ohlcv.index[int(rds.iloc[-1]['idx'])]

                    base_asset = self.ticker_info.get_base_asset(symbol)
                    if base_asset != self.quote_asset:
                        quote_asset_symbol = base_asset + self.quote_asset
                        quote_asset_price_previous = ohlcvs[(quote_asset_symbol, method.freq)].close.loc[entry_time]
                        quote_asset_price_now = ohlcvs[(quote_asset_symbol, method.freq)].close.iloc[-1]
                    else:
                        quote_asset_price_previous = 1
                        quote_asset_price_now = 1

                    value_in_btc = weight_btc / quote_asset_price_previous * quote_asset_price_now

                ret.append({
                    'symbol': symbol,
                    'method name': method.name,
                    'latest_signal': signal,
                    'weight_btc': weight_btc,
                    'freq': method.freq,
                    'return': return_,
                    'value_in_btc': value_in_btc * signal,
                    'latest_price': ohlcv.close.iloc[-1],
                    'entry_price': entry_price,
                    'entry_time': entry_time,
                    'html': htmlname,
                })

        ret = pd.DataFrame(ret)
        return ret

    def calculate_position_size(self, signals, rebalance_threshold=0.03, excluded_assets=list()):
        """Calculate the proportion of asset orders.

        Calculate data is based on latest signals dataframe.

        Args:
          signals: A dataframe of signals.
          rebalance_threshold: A float of rebalance_threshold.
          excluded_assets: A list of asset name which are excluded calculation.

        Returns:
          diff_value: A dataframe of how many assets to deposit for each cryptocurrency.
          diff_value_btc: A dataframe of converting cryptocurrency to BTC.
          transaction: A dataframe of transaction(new order) data.
        """
        if self.default_stable_coin not in excluded_assets:
            excluded_assets.append(self.default_stable_coin)

        signals['base_asset'] = signals.symbol.map(self.ticker_info.get_base_asset)
        signals['quote_asset'] = signals.symbol.map(self.ticker_info.get_quote_asset)
        signals['base_value_btc'] = signals.latest_signal * signals.value_in_btc
        signals['quote_value_btc'] = -(signals.latest_signal.astype(int) * signals.weight_btc)

        quote_asset_list = list(set(signals.quote_asset))

        # calculate base and quote assets (in btc term)
        base_asset_value = pd.Series(signals.base_value_btc.values, index=signals.base_asset)
        quote_asset_value = pd.Series(signals.quote_value_btc.values, index=signals.quote_asset)
        base_asset_value = base_asset_value.groupby(level=0).sum()
        quote_asset_value = quote_asset_value.groupby(level=0).sum()

        # get position
        position = pd.Series({i['asset']: i['free'] for i in self.ticker_info.info['balances']
                              if float(i['free']) != 0}).astype(float)
        position = position[position.index.str[:2] != 'LD']
        print(position)

        # refine asset index
        all_assets = base_asset_value.index | quote_asset_value.index | position.index

        base_asset_value = base_asset_value.reindex(all_assets).fillna(0)
        quote_asset_value = quote_asset_value.reindex(all_assets).fillna(0)
        position = position.reindex(all_assets).fillna(0)

        # calculate algo value
        algo_value_in_btc = base_asset_value + quote_asset_value
        asset_price_in_btc = position.index.map(self.ticker_info.get_asset_price_in_btc)
        algo_value = algo_value_in_btc / asset_price_in_btc

        # calculate diffierence
        margin_position = pd.Series(self._margins).reindex(all_assets).fillna(0)

        diff_value_btc = pd.DataFrame({
            'algo_p': algo_value_in_btc,
            'margin_p': margin_position * asset_price_in_btc,
            'estimate_p': algo_value_in_btc + margin_position * asset_price_in_btc,
            'present_p': position * asset_price_in_btc,
            'difference': (algo_value_in_btc + margin_position * asset_price_in_btc).clip(0,None) - position * asset_price_in_btc,
            'rebalance_threshold': (algo_value_in_btc + margin_position * asset_price_in_btc).abs() * rebalance_threshold,
        })
        diff_value_btc['rebalance'] = diff_value_btc['difference'].abs() > diff_value_btc['rebalance_threshold']
        diff_value_btc.loc[quote_asset_list, 'rebalance'] = True

        # excluding checking of asset positions

        excluded = pd.Series(True, diff_value_btc.index)
        excluded[diff_value_btc.index.isin(signals.quote_asset) | diff_value_btc.index.isin(signals.base_asset)] = False
        excluded[diff_value_btc.index.isin(excluded_assets)] = True

        diff_value_btc['excluded'] = excluded

        diff_value = diff_value_btc.copy()
        diff_value = diff_value.div(asset_price_in_btc, axis=0)
        diff_value.rebalance = diff_value.rebalance != 0
        diff_value.excluded = diff_value.excluded != 0

        # calculate transactions

        rebalance_value_btc = diff_value_btc.rebalance * diff_value_btc.difference * (~diff_value_btc.excluded)
        increase_asset_amount = rebalance_value_btc[rebalance_value_btc > 0]
        decrease_asset_amount = rebalance_value_btc[rebalance_value_btc < 0]

        diff_value_btc['rebalance'] = diff_value_btc['difference'].abs() > diff_value_btc['rebalance_threshold']
        diff_value['rebalance'] = diff_value_btc.rebalance

        txn_btc = {}

        for nai, ai in increase_asset_amount.items():
            for nad, ad in decrease_asset_amount.items():

                symbol = nad + nai
                amount = min(-ad, ai)

                is_valid = self.ticker_info._list_select(self.ticker_info.tickers, 'symbol',
                                                         symbol) is not None and nai in quote_asset_list

                if is_valid:
                    increase_asset_amount.loc[nai] -= amount
                    decrease_asset_amount.loc[nad] += amount
                    txn_btc[symbol] = -amount
                    continue

                symbol = nai + nad
                is_valid = self.ticker_info._list_select(self.ticker_info.tickers, 'symbol',
                                                         symbol) is not None and nad in quote_asset_list

                if is_valid:
                    increase_asset_amount.loc[nai] -= amount
                    decrease_asset_amount.loc[nad] += amount
                    txn_btc[symbol] = amount
                    continue

        # assumption: self.default_stable_coin can be the quote asset for all alt-coins
        transaction_btc = increase_asset_amount.append(decrease_asset_amount)
        transaction_btc.index = transaction_btc.index + self.default_stable_coin

        if self.default_stable_coin in transaction_btc.index:
            transaction_btc.pop(self.default_stable_coin+self.default_stable_coin)

        transaction_btc = transaction_btc.append(pd.Series(txn_btc))

        transaction = transaction_btc.to_frame(name='value_in_btc')
        print(transaction)
        transaction['base_asset'] = transaction.index.map(self.ticker_info.get_base_asset)
        transaction['quote_asset'] = transaction.index.map(self.ticker_info.get_quote_asset)
        transaction['value'] = transaction['value_in_btc'] / transaction.base_asset.map(
            self.ticker_info.get_asset_price_in_btc)
        transaction['price'] = transaction.index.map(
            lambda s: self.ticker_info._list_select(self.ticker_info.tickers, 'symbol', s)['price'])
        transaction = transaction.groupby(level=0).agg(
            dict(value_in_btc='sum', value='sum', base_asset='first', quote_asset='first', price='first'))
        transaction = transaction[transaction.value != 0]

        # check difference after transaction
        def asset_distributed(v):
            asset_increase = v.value_in_btc.groupby(v.base_asset).sum()
            asset_decrease = v.value_in_btc.groupby(v.quote_asset).sum()
            return asset_increase.reindex(all_assets).fillna(0) - asset_decrease.reindex(all_assets).fillna(0)

        verify_assets = asset_distributed(transaction)
        verify_assets = verify_assets[verify_assets != 0]
        verify = (verify_assets / diff_value_btc.difference.reindex(verify_assets.index) - 1).abs() < 0.001

        try:
            assert verify[verify.index != self.default_stable_coin].all()
        except:
            print(diff_value_btc)
            print(transaction)
            print(verify_assets)
            print(verify)
            raise Exception("validation fail")

        # filter out orders where base asset is in quote asset list (ex: btcusdt)
        # assumption: base asset should only be paired by one quote asset
        transaction = transaction[~(transaction.base_asset.isin(quote_asset_list) & (
                    transaction.value_in_btc.abs() < diff_value_btc.loc[
                transaction['base_asset']].rebalance_threshold.values))]

        # verify diff_value
        def get_filters(exinfo, symbol):
            filters = self.ticker_info._list_select(self.ticker_info.exinfo['symbols'], 'symbol', symbol)['filters']
            min_lot_size = self.ticker_info._list_select(filters, 'filterType', 'LOT_SIZE')['minQty']
            step_size = self.ticker_info._list_select(filters, 'filterType', 'LOT_SIZE')['stepSize']
            min_notional = self.ticker_info._list_select(filters, 'filterType', 'MIN_NOTIONAL')['minNotional']
            return {
                'min_lot_size': min_lot_size,
                'step_size': step_size,
                'min_notional': min_notional,
            }

        filters = pd.DataFrame(
            {s: get_filters(self.ticker_info.exinfo, s) for s in transaction.index}).transpose().astype(float)

        if len(transaction) != 0:
            min_notional = filters.min_notional
            minimum_lot_size = filters.min_lot_size
            step_size = filters.step_size

            # rebalance filter:
            diff = transaction['value']

            # step size filter
            diff = round((diff / step_size).astype(int) * step_size, 9)

            # minimum lot filter
            diff[diff.abs() < minimum_lot_size] = 0

            # minimum notional filter
            diff[diff.abs() * transaction.price.astype(float) < min_notional] = 0

            transaction['final_value'] = diff
            transaction['final_value_in_btc'] = diff * transaction.base_asset.map(
                self.ticker_info.get_asset_price_in_btc)
        else:
            transaction = pd.DataFrame(None, columns=['final_value'])

        transaction = transaction[transaction['final_value'] != 0]

        return diff_value, diff_value_btc, transaction

    def execute_orders(self, transactions, mode='TEST'):
        """Execute orders to Binance.

        Execute orders by program order.

        Args:
          transactions: A dataframe which is generated by transaction in calculate_position_size() function result.
          mode: A str of transactions mode, we have 3 method.
              'TEST' is simulation.
              'MARKET' is market order which is transaction at the current latest price.
              'LIMIT' is The transaction is done at the specified price. If the specified price is not touched,
                      the transaction has not been completed.

        Returns:
            A dataframe of trades.
        """
        def cancel_orders(symbol):
            orders = self._client.get_open_orders(symbol=symbol)
            for o in orders:
                self._client.cancel_order(symbol=symbol, orderId=o['orderId'])

        order_func = self._client.create_order if mode == 'MARKET' or mode == 'LIMIT' else self._client.create_test_order

        print('|---------EXECUTION LOG----------|')
        print('| time: ', datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'))

        trades = {}
        for s, lot in transactions.final_value.items():

            cancel_orders(s)

            if lot == 0:
                continue

            side = SIDE_BUY if lot > 0 else SIDE_SELL
            try:
                args = dict(
                    side=side,
                    type=ORDER_TYPE_MARKET,
                    symbol=s,
                    quantity=abs(lot))

                if mode == 'LIMIT':
                    args['price'] = transactions.price.loc[s]
                    args['type'] = ORDER_TYPE_LIMIT
                    args['timeInForce'] = 'GTC'

                order_func(**args)
                order_result = 'success'
                print('|', mode, s, side, abs(lot), order_result)
            except Exception as e:
                print('| FAIL', s, s, side, abs(lot), str(e))
                order_result = 'FAIL: ' + str(e)

            trades[s] = {
                **args,
                'result': order_result,
            }

        return pd.DataFrame(trades).transpose()

    def status(self, ohlcvs):
        """Strategy list widgets.

        Choose which strategy to implement on widgets GUI.

        Args:
          ohlcvs: A dataframe of symbol.

        Returns:
            widget GUI
        """
        import ipywidgets as widgets

        ret = pd.DataFrame()
        full_results = []
        for method in self._trading_methods:
            for symbol in method.symbols:
                ohlcv = ohlcvs[(symbol, method.freq)]
                result = method.strategy.backtest(ohlcv,
                                                  method.variables, filters=method.filters, freq=method.freq)
                ret[method.name + '-' + symbol + '-' + method.freq] = result.cumulative_returns

                weight_btc = method.weight_btc
                if isinstance(weight_btc, dict):
                    weight_btc = (weight_btc[symbol]
                        if symbol in weight_btc else weight_btc['default'])

                full_results.append({
                    'name': method.name,
                    'symbol': symbol,
                    'freq': method.freq,
                    'weight': weight_btc,
                    'portfolio': result,
                    'trading_method': method,
                    'signal': result.cash().iloc[-1] == 0,
                })

        method_dropdown = widgets.Dropdown(options=[m.name + '-' + str(i) for i, m in enumerate(self._trading_methods)])
        symbol_dropdown = widgets.Dropdown(options=[symbol + '-' + freq for symbol, freq in ohlcvs.keys()])
        backtest_btn = widgets.Button(description='status')

        backtest_panel = widgets.Output()
        option_panel = widgets.Output()

        def plotly_df(df):
            """Display plot.
            """
            # Plot
            fig = px.line()
            for sname, s in df.items():
                fig.add_scatter(x=s.index, y=s.values, name=sname)  # Not what is desired - need a line
            # fig.show()

        @backtest_panel.capture(clear_output=True)
        def backtest(_):
            """Display single strategy backtest result.
            """
            method_id = int(method_dropdown.value.split('-')[-1])
            history_id = tuple(symbol_dropdown.value.split('-'))
            ohlcv = ohlcvs[history_id]
            strategy = self._trading_methods[method_id].strategy
            svars = self._trading_methods[method_id].variables
            filters = self._trading_methods[method_id].filters
            strategy.backtest(ohlcv, variables=svars, filters=filters, freq=history_id[-1], plot=True)

        backtest_btn.on_click(backtest)
        dropdowns = widgets.HBox([method_dropdown, symbol_dropdown, backtest_btn])
        with option_panel:
            plotly_df(ret)
            display(pd.DataFrame(full_results))

        return widgets.VBox([option_panel, dropdowns, backtest_panel])

    def portfolio_backtest(self, ohlcvs, min_freq, quote_assets=['BTC', 'USDT', 'BUSD', 'USDC'], fee=0.002, delay=0):
        """Display portfolio backtest result.

        Calculate overall account asset changes.
        Unit is USD

        Args:
          ohlcvs: A dataframe of symbel.
          min_freq: A str of calculation frequency ex('4h').
          quote_assets: A list of assets name ex(['BTC', 'USDT', 'BUSD', 'ETH']).
          fee: A float of trading fee.
          delay: A int of delayed entry and exit setting.
        Returns:
            widget GUI
        """
        # backtest_results
        results = []
        for method in self._trading_methods:
            for symbol in method.symbols:
                ohlcv = ohlcvs[(symbol, method.freq)]
                result = method.strategy.backtest(ohlcv,
                                                  method.variables, filters=method.filters, freq=method.freq)
                # find weight_btc if it is in the nested dictionary
                weight_btc = method.weight_btc
                if isinstance(weight_btc, dict):
                    weight_btc = (weight_btc[symbol]
                        if symbol in weight_btc else weight_btc['default'])

                results.append({
                    'name': method.name,
                    'symbol': symbol,
                    'freq': method.freq,
                    'weight': weight_btc,
                    'portfolio': result,
                    'trading_method': method,
                    'signal': result.cash().iloc[-1] == 0,
                })

        results = pd.DataFrame(results)

        import matplotlib.pyplot as plt
        position = {}
        quote_substract = {}
        for index, value in results.transpose().items():
            position[value.loc['name'] + '|' + value.symbol + '|' + value.freq] = (value.portfolio.cash() == 0).shift(
                delay).ffill() * value.weight
        position = pd.DataFrame(position).resample(min_freq).last().ffill()
        position.columns = position.columns.str.split('|').str[1]
        position = position.ffill().fillna(0)
        position = position.groupby(position.columns, axis=1).sum()

        # find quote assets
        quote_asset_col = []
        for symbol in position.columns:
            for q in quote_assets:
                if symbol[-len(q):] == q:
                    quote_asset_col.append(q)
                    break

        quote_position = position.copy()
        quote_position.columns = quote_asset_col
        quote_position = -quote_position.groupby(quote_position.columns, axis=1).sum()

        # calculate return in usdt
        assets = position.columns.str.split('|').str[0].to_list()

        for i, a in enumerate(assets):
            for q in quote_assets:
                if len(a) > 5 and a[-len(q):] == q:
                    assets[i] = a[:-len(q)]

        position.columns = assets
        position = position.groupby(position.columns, axis=1).sum()
        quote_position = quote_position.groupby(quote_position.columns, axis=1).sum()

        all_symbols = list(set(quote_position.columns) | set(position.columns) | set(self._margins.keys()))
        if 'USDT' not in all_symbols:
            all_symbols.append('USDT')

        position = position.reindex(all_symbols, axis=1).fillna(0) + quote_position.reindex(all_symbols, axis=1).fillna(
            0)

        ohlcv_usdt = {a: get_all_binance(a + 'USDT', min_freq) for a in position.columns if a != 'USDT'}

        initial_margin_sum_btc = 0
        for a, w in self._margins.items():
            position[a] += self.ticker_info.get_asset_price_in_btc(a) * w
            initial_margin_sum_btc += self.ticker_info.get_asset_price_in_btc(a) * w

        # remove negative position
        negative_position = ((position < 0) * position).drop('USDT', axis=1, errors='ignore').sum(axis=1)
        pusdt = position['USDT'].copy()
        position = position.clip(0, None)
        position.USDT = pusdt + negative_position

        addition_usdt = -min(position.USDT.min(), 0) / self.ticker_info.get_asset_price_in_btc('USDT')

        if addition_usdt > 0:
            print('WARRN**: additional usdt is required: ', addition_usdt, ' USD')

        p = position.loc[position.index[(position != position.shift()).abs().sum(axis=1) != 0] | position.index[-1:]]
        p.index = p.index.tz_localize(None)

        ohlcv_usdt_close = pd.DataFrame({name: s.close for name, s in ohlcv_usdt.items()})
        ohlcv_usdt_close.index = ohlcv_usdt_close.index.tz_localize(None)

        rebalance_time = (p.index & ohlcv_usdt_close.index)

        ohlcv_usdt_close = ohlcv_usdt_close.loc[rebalance_time]
        p = p.loc[rebalance_time].fillna(0)

        asset_return = ((ohlcv_usdt_close.pct_change().shift(-1).fillna(0)) * p) - fee * (p - p.shift()).abs()
        asset_return.fillna(0, inplace=True)

        (asset_return.cumsum() / self.ticker_info.get_asset_price_in_btc('USDT')).plot()
        plt.show()

        s = (asset_return.sum(axis=1).cumsum() + initial_margin_sum_btc) / self.ticker_info.get_asset_price_in_btc(
            'USDT')
        s.plot()

        plt.show()

        (s / s.cummax()).plot()
        plt.show()

        return results


def render_html(signals, position, position_btc, orders, order_results):
    """Render html to google cloud platform.

    Integrate order data into tables that display in html.

    Args:
      signals_df: A dataframe of signals which are generated by TradingPortfolio().get_latest_signals().
      rebalance_df: A dataframe of diff_value which are generated by TradingPortfolio().calculate_position_size().
      rebalance_df_in_btc: A dataframe of diff_value_btc which are generated by TradingPortfolio().calculate_position_size().
      orders: A dataframe of transaction which are generated by TradingPortfolio().calculate_position_size().
      order_results: A dataframe of execute_orders which are generated by TradingPortfolio().execute_orders().

    Returns:
        html
    """
    html = """
    <!DOCTYPE html>
    <head>
      <title>Saying Hello</title>
      <link rel="stylesheet" href="https://unpkg.com/[email protected]/build/pure-min.css" integrity="sha384-cg6SkqEOCV1NbJoCu11+bm0NvBRc8IYLRGXkmNrqUBfTjmMYwNKPWBTIKyw9mHNJ" crossorigin="anonymous">
      <meta name="viewport" content="width=device-width, initial-scale=1">

    </head>
    <body style="padding: 5vw">
    """

    html += '<h1>Crypto Portfolio</h1>'
    html += '<h2>Strategy signals</h2>'
    html += signals.to_html(classes="pure-table pure-table-horizontal")
    html += '<h2>Position</h2>'
    html += position.to_html(classes="pure-table pure-table-horizontal")
    html += '<h2>Position in BTC</h2>'
    html += position_btc.to_html(classes="pure-table pure-table-horizontal")
    html += '<h2>Orders</h2>'

    if len(orders) > 0:
        orders['result'] = order_results['result']
        html += orders.to_html(classes="pure-table pure-table-horizontal")
    else:
        html += '<p>None</p>'

    html += '<br>'
    html += '<button onclick="update_position(\'MARKET\')">place market orders</button>'
    html += '<button onclick="update_position(\'LIMIT\')">place limit orders</button>'
    html += '</body>'

    html += """
  <script>
  function update_position(mode) {
    // Redirect to next page
    var next_page = window.location.href.split("?")[0] + "?mode=" + mode
    window.location = next_page;
  }
  </script>
  """
    return html

Functions

def render_html(signals, position, position_btc, orders, order_results)

Render html to google cloud platform.

Integrate order data into tables that display in html.

Args

signals_df
A dataframe of signals which are generated by TradingPortfolio().get_latest_signals().
rebalance_df
A dataframe of diff_value which are generated by TradingPortfolio().calculate_position_size().
rebalance_df_in_btc
A dataframe of diff_value_btc which are generated by TradingPortfolio().calculate_position_size().
orders
A dataframe of transaction which are generated by TradingPortfolio().calculate_position_size().
order_results
A dataframe of execute_orders which are generated by TradingPortfolio().execute_orders().

Returns

html

Expand source code
def render_html(signals, position, position_btc, orders, order_results):
    """Render html to google cloud platform.

    Integrate order data into tables that display in html.

    Args:
      signals_df: A dataframe of signals which are generated by TradingPortfolio().get_latest_signals().
      rebalance_df: A dataframe of diff_value which are generated by TradingPortfolio().calculate_position_size().
      rebalance_df_in_btc: A dataframe of diff_value_btc which are generated by TradingPortfolio().calculate_position_size().
      orders: A dataframe of transaction which are generated by TradingPortfolio().calculate_position_size().
      order_results: A dataframe of execute_orders which are generated by TradingPortfolio().execute_orders().

    Returns:
        html
    """
    html = """
    <!DOCTYPE html>
    <head>
      <title>Saying Hello</title>
      <link rel="stylesheet" href="https://unpkg.com/[email protected]/build/pure-min.css" integrity="sha384-cg6SkqEOCV1NbJoCu11+bm0NvBRc8IYLRGXkmNrqUBfTjmMYwNKPWBTIKyw9mHNJ" crossorigin="anonymous">
      <meta name="viewport" content="width=device-width, initial-scale=1">

    </head>
    <body style="padding: 5vw">
    """

    html += '<h1>Crypto Portfolio</h1>'
    html += '<h2>Strategy signals</h2>'
    html += signals.to_html(classes="pure-table pure-table-horizontal")
    html += '<h2>Position</h2>'
    html += position.to_html(classes="pure-table pure-table-horizontal")
    html += '<h2>Position in BTC</h2>'
    html += position_btc.to_html(classes="pure-table pure-table-horizontal")
    html += '<h2>Orders</h2>'

    if len(orders) > 0:
        orders['result'] = order_results['result']
        html += orders.to_html(classes="pure-table pure-table-horizontal")
    else:
        html += '<p>None</p>'

    html += '<br>'
    html += '<button onclick="update_position(\'MARKET\')">place market orders</button>'
    html += '<button onclick="update_position(\'LIMIT\')">place limit orders</button>'
    html += '</body>'

    html += """
  <script>
  function update_position(mode) {
    // Redirect to next page
    var next_page = window.location.href.split("?")[0] + "?mode=" + mode
    window.location = next_page;
  }
  </script>
  """
    return html

Classes

class TickerInfo (client)

Ticker basic info.

Get asset amount and convert price to BTC .

Attributes

client
A Binance client object where api_key, api_secret is required.
Expand source code
class TickerInfo():
    """Ticker basic info.

    Get asset amount and convert price to BTC .

    Attributes:
        client: A Binance client object where api_key, api_secret is required.

    """
    def __init__(self, client):
        self.exinfo = client.get_exchange_info()
        self.info = client.get_account()
        self.tickers = client.get_symbol_ticker()

    @staticmethod
    def _list_select(list, key, value):
        ret = [l for l in list if l[key] == value]
        if len(ret) == 0:
            return None
        else:
            return ret[0]

    def get_base_asset(self, symbol):
        """Get base asset data of a given symbol.

        Args:
          symbol: A str of trading target name.

        Returns:
            A str of base asset (ex: 'BTC').
        """
        sinfo = self._list_select(self.exinfo['symbols'], 'symbol', symbol)
        return sinfo['baseAsset']

    def get_quote_asset(self, symbol):
        """Get quote asset data of a given symbol.

        Args:
          symbol: A str of trading target name.

        Returns:
          A float of quote asset.
        """
        sinfo = self._list_select(self.exinfo['symbols'], 'symbol', symbol)
        return sinfo['quoteAsset']

    def get_asset_price_in_btc(self, asset):
        """Convert price to BTC .

        Args:
          asset: A str of asset name (ex: 'ETH').

        Returns:
          A float of price in BTC.
        """
        if asset == 'BTC':
            return 1

        ret = self._list_select(self.tickers, 'symbol', asset + 'BTC')

        if ret is not None:
            return float(ret['price'])

        ret = self._list_select(self.tickers, 'symbol', 'BTC' + asset)
        if ret is not None:
            return 1 / float(ret['price'])

        return None

Methods

def get_asset_price_in_btc(self, asset)

Convert price to BTC .

Args

asset
A str of asset name (ex: 'ETH').

Returns

A float of price in BTC.

Expand source code
def get_asset_price_in_btc(self, asset):
    """Convert price to BTC .

    Args:
      asset: A str of asset name (ex: 'ETH').

    Returns:
      A float of price in BTC.
    """
    if asset == 'BTC':
        return 1

    ret = self._list_select(self.tickers, 'symbol', asset + 'BTC')

    if ret is not None:
        return float(ret['price'])

    ret = self._list_select(self.tickers, 'symbol', 'BTC' + asset)
    if ret is not None:
        return 1 / float(ret['price'])

    return None
def get_base_asset(self, symbol)

Get base asset data of a given symbol.

Args

symbol
A str of trading target name.

Returns

A str of base asset (ex: 'BTC').

Expand source code
def get_base_asset(self, symbol):
    """Get base asset data of a given symbol.

    Args:
      symbol: A str of trading target name.

    Returns:
        A str of base asset (ex: 'BTC').
    """
    sinfo = self._list_select(self.exinfo['symbols'], 'symbol', symbol)
    return sinfo['baseAsset']
def get_quote_asset(self, symbol)

Get quote asset data of a given symbol.

Args

symbol
A str of trading target name.

Returns

A float of quote asset.

Expand source code
def get_quote_asset(self, symbol):
    """Get quote asset data of a given symbol.

    Args:
      symbol: A str of trading target name.

    Returns:
      A float of quote asset.
    """
    sinfo = self._list_select(self.exinfo['symbols'], 'symbol', symbol)
    return sinfo['quoteAsset']
class TradingMethod (symbols, freq, lookback, strategy, variables, weight_btc, filters=None, name='')

Trading method in online init setting.

Create trading method object for TradingPortfolio register .

Attributes

symbols
A list of trading pair (ex: ['USDTBTC','ETHBTC']).
freq
A str of trading time period (ex: '4h').
lookback
An int of the length of historical data (ex:1000).
strategy
A function that is your customized strategy (ex:trend_strategy).
variables
A dict of your customized strategy attributes (ex:dict(name='sma',n1=30,n2=130,),).
weight_btc
A float of btc for each commodity operation (ex: 0.2).
filters
A dict that is your customized filter (ex:{}).
name
A str of your trading method name (ex:'altcoin-trend-hullma').
Expand source code
class TradingMethod():
    """Trading method in online init setting.

    Create trading method object for TradingPortfolio register .

    Attributes:
        symbols: A list of trading pair (ex: ['USDTBTC','ETHBTC']).
        freq: A str of trading time period (ex: '4h').
        lookback: An int of the length of historical data (ex:1000).
        strategy: A function that is your customized strategy (ex:trend_strategy).
        variables: A dict of your customized strategy attributes (ex:dict(name='sma',n1=30,n2=130,),).
        weight_btc: A float of btc for each commodity operation (ex: 0.2).
        filters: A dict that is your customized filter (ex:{}).
        name: A str of your trading method name (ex:'altcoin-trend-hullma').

    """
    def __init__(self, symbols, freq, lookback, strategy, variables, weight_btc, filters=None, name=''):
        self.symbols = symbols
        self.freq = freq
        self.lookback = lookback
        self.strategy = strategy
        self.variables = variables
        self.weight_btc = weight_btc
        self.filters = filters
        self.name = name
class TradingPortfolio (binance_key, binance_secret)

Connect Binance account.

The core class to connect Binance with API, in order to connect account info, register strategt.

Attributes

binance_key
A str of is binance authorization key.
binance_secret
A str of is binance authorization secret.
Expand source code
class TradingPortfolio():
    """Connect Binance account.

    The core class to connect Binance  with API, in order to connect account info,
    register strategt.

    Attributes:
        binance_key: A str of is binance authorization key.
        binance_secret: A str of is binance authorization secret.

    """
    def __init__(self, binance_key, binance_secret):
        self._client = Client(api_key=binance_key, api_secret=binance_secret)
        self._trading_methods = []
        self._margins = {}
        self.ticker_info = TickerInfo(self._client)
        self.quote_asset = 'BTC'
        self.default_stable_coin = 'USDT'

    def set_default_stable_coin(self, token):
        self.default_stable_coin = token

    def register(self, trading_method):
        """Rigister TradingMethod object.
        Args:
          trading_method: A object of TradingMethod().
        """
        self._trading_methods.append(trading_method)

    def register_margin(self, asset, weight_btc):
        """Rigister weight_btc as operation amount.
        Args:
          asset: A str of asset name (ex: 'USDT')
          weight_btc: A float of btc for each commodity operation (ex: 0.2)
        """
        self._margins[asset] = weight_btc

    def get_all_symbol_lookback(self):
        """Get all symbol lookback.
        Use in get_ohlcvs(self) function.
        Returns:
            A dict of OHLCV lookback.

        """
        symbol_lookbacks = {}
        for method in self._trading_methods:
            for a in method.symbols:
                if (a, method.freq) not in symbol_lookbacks or method.lookback > symbol_lookbacks[(a, method.freq)]:
                    symbol_lookbacks[(a, method.freq)] = method.lookback

        # add quote asset historical data
        addition = {}
        for (symbol, freq), lookback in symbol_lookbacks.items():
            base_asset = self.ticker_info.get_base_asset(symbol)
            if base_asset != self.quote_asset:
                new_symbol = base_asset + self.quote_asset
                addition[(new_symbol, freq)] = lookback

        return {**symbol_lookbacks, **addition}

    def get_ohlcvs(self):
        """Getting histrical price data through binance api.

        Returns:
            A DataFrame of OHLCV data , the number of data length is lookback.

        """
        symbol_lookbacks = self.get_all_symbol_lookback()

        ohlcvs = {}
        for (symbol, freq), lookback in symbol_lookbacks.items():
            ohlcvs[(symbol, freq)] = get_nbars_binance(symbol, freq, lookback, self._client)

        return ohlcvs

    def get_full_ohlcvs(self):
        """Getting all histrical price data through binance api.

        Returns:
            A DataFrame of OHLCV data for all.

        """
        symbol_lookbacks = self.get_all_symbol_lookback()
        ohlcvs = {}

        for (symbol, freq), lookback in symbol_lookbacks.items():
            ohlcvs[(symbol, freq)] = get_all_binance(symbol, freq)
            time.sleep(3)
        return ohlcvs

    def get_latest_signals(self, ohlcvs, html=False):
        """Get latest signals dataframe.

        Choose which strategy to implement on widgets GUI.

        Args:
          ohlcvs: A dataframe of symbel.
          html: A bool of controlling html generation.

        Returns:
          A dataframe of latest_signals data,
          The last_signals column is bool value of whether to execute the transaction.
          The value_in_btc column is present value of assets.
        """
        ret = []
        for method in self._trading_methods:
            for symbol in method.symbols:
                ohlcv = ohlcvs[(symbol, method.freq)]
                htmlname = f'{symbol}-{method.freq}-{method.name}.html' if html else None
                result = method.strategy.backtest(ohlcv,
                                                  method.variables, filters=method.filters, plot=html,
                                                  html=htmlname,
                                                  freq=method.freq, fees=0., slippage=0.)

                signal = result.cash().iloc[-1] == 0
                return_ = 0

                # find weight_btc if it is in the nested dictionary
                weight_btc = method.weight_btc
                if isinstance(weight_btc, dict):
                    weight_btc = (weight_btc[symbol]
                        if symbol in weight_btc else weight_btc['default'])

                entry_price = 0
                entry_time = 0
                value_in_btc = 0
                if signal:
                    txn = result.positions().records
                    rds = result.orders().records
                    return_ = ohlcv.close.iloc[-1] / rds['price'].iloc[-1] - 1
                    entry_price = rds['price'].iloc[-1]
                    entry_time = ohlcv.index[int(rds.iloc[-1]['idx'])]

                    base_asset = self.ticker_info.get_base_asset(symbol)
                    if base_asset != self.quote_asset:
                        quote_asset_symbol = base_asset + self.quote_asset
                        quote_asset_price_previous = ohlcvs[(quote_asset_symbol, method.freq)].close.loc[entry_time]
                        quote_asset_price_now = ohlcvs[(quote_asset_symbol, method.freq)].close.iloc[-1]
                    else:
                        quote_asset_price_previous = 1
                        quote_asset_price_now = 1

                    value_in_btc = weight_btc / quote_asset_price_previous * quote_asset_price_now

                ret.append({
                    'symbol': symbol,
                    'method name': method.name,
                    'latest_signal': signal,
                    'weight_btc': weight_btc,
                    'freq': method.freq,
                    'return': return_,
                    'value_in_btc': value_in_btc * signal,
                    'latest_price': ohlcv.close.iloc[-1],
                    'entry_price': entry_price,
                    'entry_time': entry_time,
                    'html': htmlname,
                })

        ret = pd.DataFrame(ret)
        return ret

    def calculate_position_size(self, signals, rebalance_threshold=0.03, excluded_assets=list()):
        """Calculate the proportion of asset orders.

        Calculate data is based on latest signals dataframe.

        Args:
          signals: A dataframe of signals.
          rebalance_threshold: A float of rebalance_threshold.
          excluded_assets: A list of asset name which are excluded calculation.

        Returns:
          diff_value: A dataframe of how many assets to deposit for each cryptocurrency.
          diff_value_btc: A dataframe of converting cryptocurrency to BTC.
          transaction: A dataframe of transaction(new order) data.
        """
        if self.default_stable_coin not in excluded_assets:
            excluded_assets.append(self.default_stable_coin)

        signals['base_asset'] = signals.symbol.map(self.ticker_info.get_base_asset)
        signals['quote_asset'] = signals.symbol.map(self.ticker_info.get_quote_asset)
        signals['base_value_btc'] = signals.latest_signal * signals.value_in_btc
        signals['quote_value_btc'] = -(signals.latest_signal.astype(int) * signals.weight_btc)

        quote_asset_list = list(set(signals.quote_asset))

        # calculate base and quote assets (in btc term)
        base_asset_value = pd.Series(signals.base_value_btc.values, index=signals.base_asset)
        quote_asset_value = pd.Series(signals.quote_value_btc.values, index=signals.quote_asset)
        base_asset_value = base_asset_value.groupby(level=0).sum()
        quote_asset_value = quote_asset_value.groupby(level=0).sum()

        # get position
        position = pd.Series({i['asset']: i['free'] for i in self.ticker_info.info['balances']
                              if float(i['free']) != 0}).astype(float)
        position = position[position.index.str[:2] != 'LD']
        print(position)

        # refine asset index
        all_assets = base_asset_value.index | quote_asset_value.index | position.index

        base_asset_value = base_asset_value.reindex(all_assets).fillna(0)
        quote_asset_value = quote_asset_value.reindex(all_assets).fillna(0)
        position = position.reindex(all_assets).fillna(0)

        # calculate algo value
        algo_value_in_btc = base_asset_value + quote_asset_value
        asset_price_in_btc = position.index.map(self.ticker_info.get_asset_price_in_btc)
        algo_value = algo_value_in_btc / asset_price_in_btc

        # calculate diffierence
        margin_position = pd.Series(self._margins).reindex(all_assets).fillna(0)

        diff_value_btc = pd.DataFrame({
            'algo_p': algo_value_in_btc,
            'margin_p': margin_position * asset_price_in_btc,
            'estimate_p': algo_value_in_btc + margin_position * asset_price_in_btc,
            'present_p': position * asset_price_in_btc,
            'difference': (algo_value_in_btc + margin_position * asset_price_in_btc).clip(0,None) - position * asset_price_in_btc,
            'rebalance_threshold': (algo_value_in_btc + margin_position * asset_price_in_btc).abs() * rebalance_threshold,
        })
        diff_value_btc['rebalance'] = diff_value_btc['difference'].abs() > diff_value_btc['rebalance_threshold']
        diff_value_btc.loc[quote_asset_list, 'rebalance'] = True

        # excluding checking of asset positions

        excluded = pd.Series(True, diff_value_btc.index)
        excluded[diff_value_btc.index.isin(signals.quote_asset) | diff_value_btc.index.isin(signals.base_asset)] = False
        excluded[diff_value_btc.index.isin(excluded_assets)] = True

        diff_value_btc['excluded'] = excluded

        diff_value = diff_value_btc.copy()
        diff_value = diff_value.div(asset_price_in_btc, axis=0)
        diff_value.rebalance = diff_value.rebalance != 0
        diff_value.excluded = diff_value.excluded != 0

        # calculate transactions

        rebalance_value_btc = diff_value_btc.rebalance * diff_value_btc.difference * (~diff_value_btc.excluded)
        increase_asset_amount = rebalance_value_btc[rebalance_value_btc > 0]
        decrease_asset_amount = rebalance_value_btc[rebalance_value_btc < 0]

        diff_value_btc['rebalance'] = diff_value_btc['difference'].abs() > diff_value_btc['rebalance_threshold']
        diff_value['rebalance'] = diff_value_btc.rebalance

        txn_btc = {}

        for nai, ai in increase_asset_amount.items():
            for nad, ad in decrease_asset_amount.items():

                symbol = nad + nai
                amount = min(-ad, ai)

                is_valid = self.ticker_info._list_select(self.ticker_info.tickers, 'symbol',
                                                         symbol) is not None and nai in quote_asset_list

                if is_valid:
                    increase_asset_amount.loc[nai] -= amount
                    decrease_asset_amount.loc[nad] += amount
                    txn_btc[symbol] = -amount
                    continue

                symbol = nai + nad
                is_valid = self.ticker_info._list_select(self.ticker_info.tickers, 'symbol',
                                                         symbol) is not None and nad in quote_asset_list

                if is_valid:
                    increase_asset_amount.loc[nai] -= amount
                    decrease_asset_amount.loc[nad] += amount
                    txn_btc[symbol] = amount
                    continue

        # assumption: self.default_stable_coin can be the quote asset for all alt-coins
        transaction_btc = increase_asset_amount.append(decrease_asset_amount)
        transaction_btc.index = transaction_btc.index + self.default_stable_coin

        if self.default_stable_coin in transaction_btc.index:
            transaction_btc.pop(self.default_stable_coin+self.default_stable_coin)

        transaction_btc = transaction_btc.append(pd.Series(txn_btc))

        transaction = transaction_btc.to_frame(name='value_in_btc')
        print(transaction)
        transaction['base_asset'] = transaction.index.map(self.ticker_info.get_base_asset)
        transaction['quote_asset'] = transaction.index.map(self.ticker_info.get_quote_asset)
        transaction['value'] = transaction['value_in_btc'] / transaction.base_asset.map(
            self.ticker_info.get_asset_price_in_btc)
        transaction['price'] = transaction.index.map(
            lambda s: self.ticker_info._list_select(self.ticker_info.tickers, 'symbol', s)['price'])
        transaction = transaction.groupby(level=0).agg(
            dict(value_in_btc='sum', value='sum', base_asset='first', quote_asset='first', price='first'))
        transaction = transaction[transaction.value != 0]

        # check difference after transaction
        def asset_distributed(v):
            asset_increase = v.value_in_btc.groupby(v.base_asset).sum()
            asset_decrease = v.value_in_btc.groupby(v.quote_asset).sum()
            return asset_increase.reindex(all_assets).fillna(0) - asset_decrease.reindex(all_assets).fillna(0)

        verify_assets = asset_distributed(transaction)
        verify_assets = verify_assets[verify_assets != 0]
        verify = (verify_assets / diff_value_btc.difference.reindex(verify_assets.index) - 1).abs() < 0.001

        try:
            assert verify[verify.index != self.default_stable_coin].all()
        except:
            print(diff_value_btc)
            print(transaction)
            print(verify_assets)
            print(verify)
            raise Exception("validation fail")

        # filter out orders where base asset is in quote asset list (ex: btcusdt)
        # assumption: base asset should only be paired by one quote asset
        transaction = transaction[~(transaction.base_asset.isin(quote_asset_list) & (
                    transaction.value_in_btc.abs() < diff_value_btc.loc[
                transaction['base_asset']].rebalance_threshold.values))]

        # verify diff_value
        def get_filters(exinfo, symbol):
            filters = self.ticker_info._list_select(self.ticker_info.exinfo['symbols'], 'symbol', symbol)['filters']
            min_lot_size = self.ticker_info._list_select(filters, 'filterType', 'LOT_SIZE')['minQty']
            step_size = self.ticker_info._list_select(filters, 'filterType', 'LOT_SIZE')['stepSize']
            min_notional = self.ticker_info._list_select(filters, 'filterType', 'MIN_NOTIONAL')['minNotional']
            return {
                'min_lot_size': min_lot_size,
                'step_size': step_size,
                'min_notional': min_notional,
            }

        filters = pd.DataFrame(
            {s: get_filters(self.ticker_info.exinfo, s) for s in transaction.index}).transpose().astype(float)

        if len(transaction) != 0:
            min_notional = filters.min_notional
            minimum_lot_size = filters.min_lot_size
            step_size = filters.step_size

            # rebalance filter:
            diff = transaction['value']

            # step size filter
            diff = round((diff / step_size).astype(int) * step_size, 9)

            # minimum lot filter
            diff[diff.abs() < minimum_lot_size] = 0

            # minimum notional filter
            diff[diff.abs() * transaction.price.astype(float) < min_notional] = 0

            transaction['final_value'] = diff
            transaction['final_value_in_btc'] = diff * transaction.base_asset.map(
                self.ticker_info.get_asset_price_in_btc)
        else:
            transaction = pd.DataFrame(None, columns=['final_value'])

        transaction = transaction[transaction['final_value'] != 0]

        return diff_value, diff_value_btc, transaction

    def execute_orders(self, transactions, mode='TEST'):
        """Execute orders to Binance.

        Execute orders by program order.

        Args:
          transactions: A dataframe which is generated by transaction in calculate_position_size() function result.
          mode: A str of transactions mode, we have 3 method.
              'TEST' is simulation.
              'MARKET' is market order which is transaction at the current latest price.
              'LIMIT' is The transaction is done at the specified price. If the specified price is not touched,
                      the transaction has not been completed.

        Returns:
            A dataframe of trades.
        """
        def cancel_orders(symbol):
            orders = self._client.get_open_orders(symbol=symbol)
            for o in orders:
                self._client.cancel_order(symbol=symbol, orderId=o['orderId'])

        order_func = self._client.create_order if mode == 'MARKET' or mode == 'LIMIT' else self._client.create_test_order

        print('|---------EXECUTION LOG----------|')
        print('| time: ', datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'))

        trades = {}
        for s, lot in transactions.final_value.items():

            cancel_orders(s)

            if lot == 0:
                continue

            side = SIDE_BUY if lot > 0 else SIDE_SELL
            try:
                args = dict(
                    side=side,
                    type=ORDER_TYPE_MARKET,
                    symbol=s,
                    quantity=abs(lot))

                if mode == 'LIMIT':
                    args['price'] = transactions.price.loc[s]
                    args['type'] = ORDER_TYPE_LIMIT
                    args['timeInForce'] = 'GTC'

                order_func(**args)
                order_result = 'success'
                print('|', mode, s, side, abs(lot), order_result)
            except Exception as e:
                print('| FAIL', s, s, side, abs(lot), str(e))
                order_result = 'FAIL: ' + str(e)

            trades[s] = {
                **args,
                'result': order_result,
            }

        return pd.DataFrame(trades).transpose()

    def status(self, ohlcvs):
        """Strategy list widgets.

        Choose which strategy to implement on widgets GUI.

        Args:
          ohlcvs: A dataframe of symbol.

        Returns:
            widget GUI
        """
        import ipywidgets as widgets

        ret = pd.DataFrame()
        full_results = []
        for method in self._trading_methods:
            for symbol in method.symbols:
                ohlcv = ohlcvs[(symbol, method.freq)]
                result = method.strategy.backtest(ohlcv,
                                                  method.variables, filters=method.filters, freq=method.freq)
                ret[method.name + '-' + symbol + '-' + method.freq] = result.cumulative_returns

                weight_btc = method.weight_btc
                if isinstance(weight_btc, dict):
                    weight_btc = (weight_btc[symbol]
                        if symbol in weight_btc else weight_btc['default'])

                full_results.append({
                    'name': method.name,
                    'symbol': symbol,
                    'freq': method.freq,
                    'weight': weight_btc,
                    'portfolio': result,
                    'trading_method': method,
                    'signal': result.cash().iloc[-1] == 0,
                })

        method_dropdown = widgets.Dropdown(options=[m.name + '-' + str(i) for i, m in enumerate(self._trading_methods)])
        symbol_dropdown = widgets.Dropdown(options=[symbol + '-' + freq for symbol, freq in ohlcvs.keys()])
        backtest_btn = widgets.Button(description='status')

        backtest_panel = widgets.Output()
        option_panel = widgets.Output()

        def plotly_df(df):
            """Display plot.
            """
            # Plot
            fig = px.line()
            for sname, s in df.items():
                fig.add_scatter(x=s.index, y=s.values, name=sname)  # Not what is desired - need a line
            # fig.show()

        @backtest_panel.capture(clear_output=True)
        def backtest(_):
            """Display single strategy backtest result.
            """
            method_id = int(method_dropdown.value.split('-')[-1])
            history_id = tuple(symbol_dropdown.value.split('-'))
            ohlcv = ohlcvs[history_id]
            strategy = self._trading_methods[method_id].strategy
            svars = self._trading_methods[method_id].variables
            filters = self._trading_methods[method_id].filters
            strategy.backtest(ohlcv, variables=svars, filters=filters, freq=history_id[-1], plot=True)

        backtest_btn.on_click(backtest)
        dropdowns = widgets.HBox([method_dropdown, symbol_dropdown, backtest_btn])
        with option_panel:
            plotly_df(ret)
            display(pd.DataFrame(full_results))

        return widgets.VBox([option_panel, dropdowns, backtest_panel])

    def portfolio_backtest(self, ohlcvs, min_freq, quote_assets=['BTC', 'USDT', 'BUSD', 'USDC'], fee=0.002, delay=0):
        """Display portfolio backtest result.

        Calculate overall account asset changes.
        Unit is USD

        Args:
          ohlcvs: A dataframe of symbel.
          min_freq: A str of calculation frequency ex('4h').
          quote_assets: A list of assets name ex(['BTC', 'USDT', 'BUSD', 'ETH']).
          fee: A float of trading fee.
          delay: A int of delayed entry and exit setting.
        Returns:
            widget GUI
        """
        # backtest_results
        results = []
        for method in self._trading_methods:
            for symbol in method.symbols:
                ohlcv = ohlcvs[(symbol, method.freq)]
                result = method.strategy.backtest(ohlcv,
                                                  method.variables, filters=method.filters, freq=method.freq)
                # find weight_btc if it is in the nested dictionary
                weight_btc = method.weight_btc
                if isinstance(weight_btc, dict):
                    weight_btc = (weight_btc[symbol]
                        if symbol in weight_btc else weight_btc['default'])

                results.append({
                    'name': method.name,
                    'symbol': symbol,
                    'freq': method.freq,
                    'weight': weight_btc,
                    'portfolio': result,
                    'trading_method': method,
                    'signal': result.cash().iloc[-1] == 0,
                })

        results = pd.DataFrame(results)

        import matplotlib.pyplot as plt
        position = {}
        quote_substract = {}
        for index, value in results.transpose().items():
            position[value.loc['name'] + '|' + value.symbol + '|' + value.freq] = (value.portfolio.cash() == 0).shift(
                delay).ffill() * value.weight
        position = pd.DataFrame(position).resample(min_freq).last().ffill()
        position.columns = position.columns.str.split('|').str[1]
        position = position.ffill().fillna(0)
        position = position.groupby(position.columns, axis=1).sum()

        # find quote assets
        quote_asset_col = []
        for symbol in position.columns:
            for q in quote_assets:
                if symbol[-len(q):] == q:
                    quote_asset_col.append(q)
                    break

        quote_position = position.copy()
        quote_position.columns = quote_asset_col
        quote_position = -quote_position.groupby(quote_position.columns, axis=1).sum()

        # calculate return in usdt
        assets = position.columns.str.split('|').str[0].to_list()

        for i, a in enumerate(assets):
            for q in quote_assets:
                if len(a) > 5 and a[-len(q):] == q:
                    assets[i] = a[:-len(q)]

        position.columns = assets
        position = position.groupby(position.columns, axis=1).sum()
        quote_position = quote_position.groupby(quote_position.columns, axis=1).sum()

        all_symbols = list(set(quote_position.columns) | set(position.columns) | set(self._margins.keys()))
        if 'USDT' not in all_symbols:
            all_symbols.append('USDT')

        position = position.reindex(all_symbols, axis=1).fillna(0) + quote_position.reindex(all_symbols, axis=1).fillna(
            0)

        ohlcv_usdt = {a: get_all_binance(a + 'USDT', min_freq) for a in position.columns if a != 'USDT'}

        initial_margin_sum_btc = 0
        for a, w in self._margins.items():
            position[a] += self.ticker_info.get_asset_price_in_btc(a) * w
            initial_margin_sum_btc += self.ticker_info.get_asset_price_in_btc(a) * w

        # remove negative position
        negative_position = ((position < 0) * position).drop('USDT', axis=1, errors='ignore').sum(axis=1)
        pusdt = position['USDT'].copy()
        position = position.clip(0, None)
        position.USDT = pusdt + negative_position

        addition_usdt = -min(position.USDT.min(), 0) / self.ticker_info.get_asset_price_in_btc('USDT')

        if addition_usdt > 0:
            print('WARRN**: additional usdt is required: ', addition_usdt, ' USD')

        p = position.loc[position.index[(position != position.shift()).abs().sum(axis=1) != 0] | position.index[-1:]]
        p.index = p.index.tz_localize(None)

        ohlcv_usdt_close = pd.DataFrame({name: s.close for name, s in ohlcv_usdt.items()})
        ohlcv_usdt_close.index = ohlcv_usdt_close.index.tz_localize(None)

        rebalance_time = (p.index & ohlcv_usdt_close.index)

        ohlcv_usdt_close = ohlcv_usdt_close.loc[rebalance_time]
        p = p.loc[rebalance_time].fillna(0)

        asset_return = ((ohlcv_usdt_close.pct_change().shift(-1).fillna(0)) * p) - fee * (p - p.shift()).abs()
        asset_return.fillna(0, inplace=True)

        (asset_return.cumsum() / self.ticker_info.get_asset_price_in_btc('USDT')).plot()
        plt.show()

        s = (asset_return.sum(axis=1).cumsum() + initial_margin_sum_btc) / self.ticker_info.get_asset_price_in_btc(
            'USDT')
        s.plot()

        plt.show()

        (s / s.cummax()).plot()
        plt.show()

        return results

Methods

def calculate_position_size(self, signals, rebalance_threshold=0.03, excluded_assets=[])

Calculate the proportion of asset orders.

Calculate data is based on latest signals dataframe.

Args

signals
A dataframe of signals.
rebalance_threshold
A float of rebalance_threshold.
excluded_assets
A list of asset name which are excluded calculation.

Returns

diff_value
A dataframe of how many assets to deposit for each cryptocurrency.
diff_value_btc
A dataframe of converting cryptocurrency to BTC.
transaction
A dataframe of transaction(new order) data.
Expand source code
def calculate_position_size(self, signals, rebalance_threshold=0.03, excluded_assets=list()):
    """Calculate the proportion of asset orders.

    Calculate data is based on latest signals dataframe.

    Args:
      signals: A dataframe of signals.
      rebalance_threshold: A float of rebalance_threshold.
      excluded_assets: A list of asset name which are excluded calculation.

    Returns:
      diff_value: A dataframe of how many assets to deposit for each cryptocurrency.
      diff_value_btc: A dataframe of converting cryptocurrency to BTC.
      transaction: A dataframe of transaction(new order) data.
    """
    if self.default_stable_coin not in excluded_assets:
        excluded_assets.append(self.default_stable_coin)

    signals['base_asset'] = signals.symbol.map(self.ticker_info.get_base_asset)
    signals['quote_asset'] = signals.symbol.map(self.ticker_info.get_quote_asset)
    signals['base_value_btc'] = signals.latest_signal * signals.value_in_btc
    signals['quote_value_btc'] = -(signals.latest_signal.astype(int) * signals.weight_btc)

    quote_asset_list = list(set(signals.quote_asset))

    # calculate base and quote assets (in btc term)
    base_asset_value = pd.Series(signals.base_value_btc.values, index=signals.base_asset)
    quote_asset_value = pd.Series(signals.quote_value_btc.values, index=signals.quote_asset)
    base_asset_value = base_asset_value.groupby(level=0).sum()
    quote_asset_value = quote_asset_value.groupby(level=0).sum()

    # get position
    position = pd.Series({i['asset']: i['free'] for i in self.ticker_info.info['balances']
                          if float(i['free']) != 0}).astype(float)
    position = position[position.index.str[:2] != 'LD']
    print(position)

    # refine asset index
    all_assets = base_asset_value.index | quote_asset_value.index | position.index

    base_asset_value = base_asset_value.reindex(all_assets).fillna(0)
    quote_asset_value = quote_asset_value.reindex(all_assets).fillna(0)
    position = position.reindex(all_assets).fillna(0)

    # calculate algo value
    algo_value_in_btc = base_asset_value + quote_asset_value
    asset_price_in_btc = position.index.map(self.ticker_info.get_asset_price_in_btc)
    algo_value = algo_value_in_btc / asset_price_in_btc

    # calculate diffierence
    margin_position = pd.Series(self._margins).reindex(all_assets).fillna(0)

    diff_value_btc = pd.DataFrame({
        'algo_p': algo_value_in_btc,
        'margin_p': margin_position * asset_price_in_btc,
        'estimate_p': algo_value_in_btc + margin_position * asset_price_in_btc,
        'present_p': position * asset_price_in_btc,
        'difference': (algo_value_in_btc + margin_position * asset_price_in_btc).clip(0,None) - position * asset_price_in_btc,
        'rebalance_threshold': (algo_value_in_btc + margin_position * asset_price_in_btc).abs() * rebalance_threshold,
    })
    diff_value_btc['rebalance'] = diff_value_btc['difference'].abs() > diff_value_btc['rebalance_threshold']
    diff_value_btc.loc[quote_asset_list, 'rebalance'] = True

    # excluding checking of asset positions

    excluded = pd.Series(True, diff_value_btc.index)
    excluded[diff_value_btc.index.isin(signals.quote_asset) | diff_value_btc.index.isin(signals.base_asset)] = False
    excluded[diff_value_btc.index.isin(excluded_assets)] = True

    diff_value_btc['excluded'] = excluded

    diff_value = diff_value_btc.copy()
    diff_value = diff_value.div(asset_price_in_btc, axis=0)
    diff_value.rebalance = diff_value.rebalance != 0
    diff_value.excluded = diff_value.excluded != 0

    # calculate transactions

    rebalance_value_btc = diff_value_btc.rebalance * diff_value_btc.difference * (~diff_value_btc.excluded)
    increase_asset_amount = rebalance_value_btc[rebalance_value_btc > 0]
    decrease_asset_amount = rebalance_value_btc[rebalance_value_btc < 0]

    diff_value_btc['rebalance'] = diff_value_btc['difference'].abs() > diff_value_btc['rebalance_threshold']
    diff_value['rebalance'] = diff_value_btc.rebalance

    txn_btc = {}

    for nai, ai in increase_asset_amount.items():
        for nad, ad in decrease_asset_amount.items():

            symbol = nad + nai
            amount = min(-ad, ai)

            is_valid = self.ticker_info._list_select(self.ticker_info.tickers, 'symbol',
                                                     symbol) is not None and nai in quote_asset_list

            if is_valid:
                increase_asset_amount.loc[nai] -= amount
                decrease_asset_amount.loc[nad] += amount
                txn_btc[symbol] = -amount
                continue

            symbol = nai + nad
            is_valid = self.ticker_info._list_select(self.ticker_info.tickers, 'symbol',
                                                     symbol) is not None and nad in quote_asset_list

            if is_valid:
                increase_asset_amount.loc[nai] -= amount
                decrease_asset_amount.loc[nad] += amount
                txn_btc[symbol] = amount
                continue

    # assumption: self.default_stable_coin can be the quote asset for all alt-coins
    transaction_btc = increase_asset_amount.append(decrease_asset_amount)
    transaction_btc.index = transaction_btc.index + self.default_stable_coin

    if self.default_stable_coin in transaction_btc.index:
        transaction_btc.pop(self.default_stable_coin+self.default_stable_coin)

    transaction_btc = transaction_btc.append(pd.Series(txn_btc))

    transaction = transaction_btc.to_frame(name='value_in_btc')
    print(transaction)
    transaction['base_asset'] = transaction.index.map(self.ticker_info.get_base_asset)
    transaction['quote_asset'] = transaction.index.map(self.ticker_info.get_quote_asset)
    transaction['value'] = transaction['value_in_btc'] / transaction.base_asset.map(
        self.ticker_info.get_asset_price_in_btc)
    transaction['price'] = transaction.index.map(
        lambda s: self.ticker_info._list_select(self.ticker_info.tickers, 'symbol', s)['price'])
    transaction = transaction.groupby(level=0).agg(
        dict(value_in_btc='sum', value='sum', base_asset='first', quote_asset='first', price='first'))
    transaction = transaction[transaction.value != 0]

    # check difference after transaction
    def asset_distributed(v):
        asset_increase = v.value_in_btc.groupby(v.base_asset).sum()
        asset_decrease = v.value_in_btc.groupby(v.quote_asset).sum()
        return asset_increase.reindex(all_assets).fillna(0) - asset_decrease.reindex(all_assets).fillna(0)

    verify_assets = asset_distributed(transaction)
    verify_assets = verify_assets[verify_assets != 0]
    verify = (verify_assets / diff_value_btc.difference.reindex(verify_assets.index) - 1).abs() < 0.001

    try:
        assert verify[verify.index != self.default_stable_coin].all()
    except:
        print(diff_value_btc)
        print(transaction)
        print(verify_assets)
        print(verify)
        raise Exception("validation fail")

    # filter out orders where base asset is in quote asset list (ex: btcusdt)
    # assumption: base asset should only be paired by one quote asset
    transaction = transaction[~(transaction.base_asset.isin(quote_asset_list) & (
                transaction.value_in_btc.abs() < diff_value_btc.loc[
            transaction['base_asset']].rebalance_threshold.values))]

    # verify diff_value
    def get_filters(exinfo, symbol):
        filters = self.ticker_info._list_select(self.ticker_info.exinfo['symbols'], 'symbol', symbol)['filters']
        min_lot_size = self.ticker_info._list_select(filters, 'filterType', 'LOT_SIZE')['minQty']
        step_size = self.ticker_info._list_select(filters, 'filterType', 'LOT_SIZE')['stepSize']
        min_notional = self.ticker_info._list_select(filters, 'filterType', 'MIN_NOTIONAL')['minNotional']
        return {
            'min_lot_size': min_lot_size,
            'step_size': step_size,
            'min_notional': min_notional,
        }

    filters = pd.DataFrame(
        {s: get_filters(self.ticker_info.exinfo, s) for s in transaction.index}).transpose().astype(float)

    if len(transaction) != 0:
        min_notional = filters.min_notional
        minimum_lot_size = filters.min_lot_size
        step_size = filters.step_size

        # rebalance filter:
        diff = transaction['value']

        # step size filter
        diff = round((diff / step_size).astype(int) * step_size, 9)

        # minimum lot filter
        diff[diff.abs() < minimum_lot_size] = 0

        # minimum notional filter
        diff[diff.abs() * transaction.price.astype(float) < min_notional] = 0

        transaction['final_value'] = diff
        transaction['final_value_in_btc'] = diff * transaction.base_asset.map(
            self.ticker_info.get_asset_price_in_btc)
    else:
        transaction = pd.DataFrame(None, columns=['final_value'])

    transaction = transaction[transaction['final_value'] != 0]

    return diff_value, diff_value_btc, transaction
def execute_orders(self, transactions, mode='TEST')

Execute orders to Binance.

Execute orders by program order.

Args

transactions
A dataframe which is generated by transaction in calculate_position_size() function result.
mode
A str of transactions mode, we have 3 method. 'TEST' is simulation. 'MARKET' is market order which is transaction at the current latest price. 'LIMIT' is The transaction is done at the specified price. If the specified price is not touched, the transaction has not been completed.

Returns

A dataframe of trades.

Expand source code
def execute_orders(self, transactions, mode='TEST'):
    """Execute orders to Binance.

    Execute orders by program order.

    Args:
      transactions: A dataframe which is generated by transaction in calculate_position_size() function result.
      mode: A str of transactions mode, we have 3 method.
          'TEST' is simulation.
          'MARKET' is market order which is transaction at the current latest price.
          'LIMIT' is The transaction is done at the specified price. If the specified price is not touched,
                  the transaction has not been completed.

    Returns:
        A dataframe of trades.
    """
    def cancel_orders(symbol):
        orders = self._client.get_open_orders(symbol=symbol)
        for o in orders:
            self._client.cancel_order(symbol=symbol, orderId=o['orderId'])

    order_func = self._client.create_order if mode == 'MARKET' or mode == 'LIMIT' else self._client.create_test_order

    print('|---------EXECUTION LOG----------|')
    print('| time: ', datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'))

    trades = {}
    for s, lot in transactions.final_value.items():

        cancel_orders(s)

        if lot == 0:
            continue

        side = SIDE_BUY if lot > 0 else SIDE_SELL
        try:
            args = dict(
                side=side,
                type=ORDER_TYPE_MARKET,
                symbol=s,
                quantity=abs(lot))

            if mode == 'LIMIT':
                args['price'] = transactions.price.loc[s]
                args['type'] = ORDER_TYPE_LIMIT
                args['timeInForce'] = 'GTC'

            order_func(**args)
            order_result = 'success'
            print('|', mode, s, side, abs(lot), order_result)
        except Exception as e:
            print('| FAIL', s, s, side, abs(lot), str(e))
            order_result = 'FAIL: ' + str(e)

        trades[s] = {
            **args,
            'result': order_result,
        }

    return pd.DataFrame(trades).transpose()
def get_all_symbol_lookback(self)

Get all symbol lookback. Use in get_ohlcvs(self) function.

Returns

A dict of OHLCV lookback.

Expand source code
def get_all_symbol_lookback(self):
    """Get all symbol lookback.
    Use in get_ohlcvs(self) function.
    Returns:
        A dict of OHLCV lookback.

    """
    symbol_lookbacks = {}
    for method in self._trading_methods:
        for a in method.symbols:
            if (a, method.freq) not in symbol_lookbacks or method.lookback > symbol_lookbacks[(a, method.freq)]:
                symbol_lookbacks[(a, method.freq)] = method.lookback

    # add quote asset historical data
    addition = {}
    for (symbol, freq), lookback in symbol_lookbacks.items():
        base_asset = self.ticker_info.get_base_asset(symbol)
        if base_asset != self.quote_asset:
            new_symbol = base_asset + self.quote_asset
            addition[(new_symbol, freq)] = lookback

    return {**symbol_lookbacks, **addition}
def get_full_ohlcvs(self)

Getting all histrical price data through binance api.

Returns

A DataFrame of OHLCV data for all.

Expand source code
def get_full_ohlcvs(self):
    """Getting all histrical price data through binance api.

    Returns:
        A DataFrame of OHLCV data for all.

    """
    symbol_lookbacks = self.get_all_symbol_lookback()
    ohlcvs = {}

    for (symbol, freq), lookback in symbol_lookbacks.items():
        ohlcvs[(symbol, freq)] = get_all_binance(symbol, freq)
        time.sleep(3)
    return ohlcvs
def get_latest_signals(self, ohlcvs, html=False)

Get latest signals dataframe.

Choose which strategy to implement on widgets GUI.

Args

ohlcvs
A dataframe of symbel.
html
A bool of controlling html generation.

Returns

A dataframe of latest_signals data, The last_signals column is bool value of whether to execute the transaction. The value_in_btc column is present value of assets.

Expand source code
def get_latest_signals(self, ohlcvs, html=False):
    """Get latest signals dataframe.

    Choose which strategy to implement on widgets GUI.

    Args:
      ohlcvs: A dataframe of symbel.
      html: A bool of controlling html generation.

    Returns:
      A dataframe of latest_signals data,
      The last_signals column is bool value of whether to execute the transaction.
      The value_in_btc column is present value of assets.
    """
    ret = []
    for method in self._trading_methods:
        for symbol in method.symbols:
            ohlcv = ohlcvs[(symbol, method.freq)]
            htmlname = f'{symbol}-{method.freq}-{method.name}.html' if html else None
            result = method.strategy.backtest(ohlcv,
                                              method.variables, filters=method.filters, plot=html,
                                              html=htmlname,
                                              freq=method.freq, fees=0., slippage=0.)

            signal = result.cash().iloc[-1] == 0
            return_ = 0

            # find weight_btc if it is in the nested dictionary
            weight_btc = method.weight_btc
            if isinstance(weight_btc, dict):
                weight_btc = (weight_btc[symbol]
                    if symbol in weight_btc else weight_btc['default'])

            entry_price = 0
            entry_time = 0
            value_in_btc = 0
            if signal:
                txn = result.positions().records
                rds = result.orders().records
                return_ = ohlcv.close.iloc[-1] / rds['price'].iloc[-1] - 1
                entry_price = rds['price'].iloc[-1]
                entry_time = ohlcv.index[int(rds.iloc[-1]['idx'])]

                base_asset = self.ticker_info.get_base_asset(symbol)
                if base_asset != self.quote_asset:
                    quote_asset_symbol = base_asset + self.quote_asset
                    quote_asset_price_previous = ohlcvs[(quote_asset_symbol, method.freq)].close.loc[entry_time]
                    quote_asset_price_now = ohlcvs[(quote_asset_symbol, method.freq)].close.iloc[-1]
                else:
                    quote_asset_price_previous = 1
                    quote_asset_price_now = 1

                value_in_btc = weight_btc / quote_asset_price_previous * quote_asset_price_now

            ret.append({
                'symbol': symbol,
                'method name': method.name,
                'latest_signal': signal,
                'weight_btc': weight_btc,
                'freq': method.freq,
                'return': return_,
                'value_in_btc': value_in_btc * signal,
                'latest_price': ohlcv.close.iloc[-1],
                'entry_price': entry_price,
                'entry_time': entry_time,
                'html': htmlname,
            })

    ret = pd.DataFrame(ret)
    return ret
def get_ohlcvs(self)

Getting histrical price data through binance api.

Returns

A DataFrame of OHLCV data , the number of data length is lookback.

Expand source code
def get_ohlcvs(self):
    """Getting histrical price data through binance api.

    Returns:
        A DataFrame of OHLCV data , the number of data length is lookback.

    """
    symbol_lookbacks = self.get_all_symbol_lookback()

    ohlcvs = {}
    for (symbol, freq), lookback in symbol_lookbacks.items():
        ohlcvs[(symbol, freq)] = get_nbars_binance(symbol, freq, lookback, self._client)

    return ohlcvs
def portfolio_backtest(self, ohlcvs, min_freq, quote_assets=['BTC', 'USDT', 'BUSD', 'USDC'], fee=0.002, delay=0)

Display portfolio backtest result.

Calculate overall account asset changes. Unit is USD

Args

ohlcvs
A dataframe of symbel.
min_freq
A str of calculation frequency ex('4h').
quote_assets
A list of assets name ex(['BTC', 'USDT', 'BUSD', 'ETH']).
fee
A float of trading fee.
delay
A int of delayed entry and exit setting.

Returns

widget GUI

Expand source code
def portfolio_backtest(self, ohlcvs, min_freq, quote_assets=['BTC', 'USDT', 'BUSD', 'USDC'], fee=0.002, delay=0):
    """Display portfolio backtest result.

    Calculate overall account asset changes.
    Unit is USD

    Args:
      ohlcvs: A dataframe of symbel.
      min_freq: A str of calculation frequency ex('4h').
      quote_assets: A list of assets name ex(['BTC', 'USDT', 'BUSD', 'ETH']).
      fee: A float of trading fee.
      delay: A int of delayed entry and exit setting.
    Returns:
        widget GUI
    """
    # backtest_results
    results = []
    for method in self._trading_methods:
        for symbol in method.symbols:
            ohlcv = ohlcvs[(symbol, method.freq)]
            result = method.strategy.backtest(ohlcv,
                                              method.variables, filters=method.filters, freq=method.freq)
            # find weight_btc if it is in the nested dictionary
            weight_btc = method.weight_btc
            if isinstance(weight_btc, dict):
                weight_btc = (weight_btc[symbol]
                    if symbol in weight_btc else weight_btc['default'])

            results.append({
                'name': method.name,
                'symbol': symbol,
                'freq': method.freq,
                'weight': weight_btc,
                'portfolio': result,
                'trading_method': method,
                'signal': result.cash().iloc[-1] == 0,
            })

    results = pd.DataFrame(results)

    import matplotlib.pyplot as plt
    position = {}
    quote_substract = {}
    for index, value in results.transpose().items():
        position[value.loc['name'] + '|' + value.symbol + '|' + value.freq] = (value.portfolio.cash() == 0).shift(
            delay).ffill() * value.weight
    position = pd.DataFrame(position).resample(min_freq).last().ffill()
    position.columns = position.columns.str.split('|').str[1]
    position = position.ffill().fillna(0)
    position = position.groupby(position.columns, axis=1).sum()

    # find quote assets
    quote_asset_col = []
    for symbol in position.columns:
        for q in quote_assets:
            if symbol[-len(q):] == q:
                quote_asset_col.append(q)
                break

    quote_position = position.copy()
    quote_position.columns = quote_asset_col
    quote_position = -quote_position.groupby(quote_position.columns, axis=1).sum()

    # calculate return in usdt
    assets = position.columns.str.split('|').str[0].to_list()

    for i, a in enumerate(assets):
        for q in quote_assets:
            if len(a) > 5 and a[-len(q):] == q:
                assets[i] = a[:-len(q)]

    position.columns = assets
    position = position.groupby(position.columns, axis=1).sum()
    quote_position = quote_position.groupby(quote_position.columns, axis=1).sum()

    all_symbols = list(set(quote_position.columns) | set(position.columns) | set(self._margins.keys()))
    if 'USDT' not in all_symbols:
        all_symbols.append('USDT')

    position = position.reindex(all_symbols, axis=1).fillna(0) + quote_position.reindex(all_symbols, axis=1).fillna(
        0)

    ohlcv_usdt = {a: get_all_binance(a + 'USDT', min_freq) for a in position.columns if a != 'USDT'}

    initial_margin_sum_btc = 0
    for a, w in self._margins.items():
        position[a] += self.ticker_info.get_asset_price_in_btc(a) * w
        initial_margin_sum_btc += self.ticker_info.get_asset_price_in_btc(a) * w

    # remove negative position
    negative_position = ((position < 0) * position).drop('USDT', axis=1, errors='ignore').sum(axis=1)
    pusdt = position['USDT'].copy()
    position = position.clip(0, None)
    position.USDT = pusdt + negative_position

    addition_usdt = -min(position.USDT.min(), 0) / self.ticker_info.get_asset_price_in_btc('USDT')

    if addition_usdt > 0:
        print('WARRN**: additional usdt is required: ', addition_usdt, ' USD')

    p = position.loc[position.index[(position != position.shift()).abs().sum(axis=1) != 0] | position.index[-1:]]
    p.index = p.index.tz_localize(None)

    ohlcv_usdt_close = pd.DataFrame({name: s.close for name, s in ohlcv_usdt.items()})
    ohlcv_usdt_close.index = ohlcv_usdt_close.index.tz_localize(None)

    rebalance_time = (p.index & ohlcv_usdt_close.index)

    ohlcv_usdt_close = ohlcv_usdt_close.loc[rebalance_time]
    p = p.loc[rebalance_time].fillna(0)

    asset_return = ((ohlcv_usdt_close.pct_change().shift(-1).fillna(0)) * p) - fee * (p - p.shift()).abs()
    asset_return.fillna(0, inplace=True)

    (asset_return.cumsum() / self.ticker_info.get_asset_price_in_btc('USDT')).plot()
    plt.show()

    s = (asset_return.sum(axis=1).cumsum() + initial_margin_sum_btc) / self.ticker_info.get_asset_price_in_btc(
        'USDT')
    s.plot()

    plt.show()

    (s / s.cummax()).plot()
    plt.show()

    return results
def register(self, trading_method)

Rigister TradingMethod object.

Args

trading_method
A object of TradingMethod().
Expand source code
def register(self, trading_method):
    """Rigister TradingMethod object.
    Args:
      trading_method: A object of TradingMethod().
    """
    self._trading_methods.append(trading_method)
def register_margin(self, asset, weight_btc)

Rigister weight_btc as operation amount.

Args

asset
A str of asset name (ex: 'USDT')
weight_btc
A float of btc for each commodity operation (ex: 0.2)
Expand source code
def register_margin(self, asset, weight_btc):
    """Rigister weight_btc as operation amount.
    Args:
      asset: A str of asset name (ex: 'USDT')
      weight_btc: A float of btc for each commodity operation (ex: 0.2)
    """
    self._margins[asset] = weight_btc
def set_default_stable_coin(self, token)
Expand source code
def set_default_stable_coin(self, token):
    self.default_stable_coin = token
def status(self, ohlcvs)

Strategy list widgets.

Choose which strategy to implement on widgets GUI.

Args

ohlcvs
A dataframe of symbol.

Returns

widget GUI

Expand source code
def status(self, ohlcvs):
    """Strategy list widgets.

    Choose which strategy to implement on widgets GUI.

    Args:
      ohlcvs: A dataframe of symbol.

    Returns:
        widget GUI
    """
    import ipywidgets as widgets

    ret = pd.DataFrame()
    full_results = []
    for method in self._trading_methods:
        for symbol in method.symbols:
            ohlcv = ohlcvs[(symbol, method.freq)]
            result = method.strategy.backtest(ohlcv,
                                              method.variables, filters=method.filters, freq=method.freq)
            ret[method.name + '-' + symbol + '-' + method.freq] = result.cumulative_returns

            weight_btc = method.weight_btc
            if isinstance(weight_btc, dict):
                weight_btc = (weight_btc[symbol]
                    if symbol in weight_btc else weight_btc['default'])

            full_results.append({
                'name': method.name,
                'symbol': symbol,
                'freq': method.freq,
                'weight': weight_btc,
                'portfolio': result,
                'trading_method': method,
                'signal': result.cash().iloc[-1] == 0,
            })

    method_dropdown = widgets.Dropdown(options=[m.name + '-' + str(i) for i, m in enumerate(self._trading_methods)])
    symbol_dropdown = widgets.Dropdown(options=[symbol + '-' + freq for symbol, freq in ohlcvs.keys()])
    backtest_btn = widgets.Button(description='status')

    backtest_panel = widgets.Output()
    option_panel = widgets.Output()

    def plotly_df(df):
        """Display plot.
        """
        # Plot
        fig = px.line()
        for sname, s in df.items():
            fig.add_scatter(x=s.index, y=s.values, name=sname)  # Not what is desired - need a line
        # fig.show()

    @backtest_panel.capture(clear_output=True)
    def backtest(_):
        """Display single strategy backtest result.
        """
        method_id = int(method_dropdown.value.split('-')[-1])
        history_id = tuple(symbol_dropdown.value.split('-'))
        ohlcv = ohlcvs[history_id]
        strategy = self._trading_methods[method_id].strategy
        svars = self._trading_methods[method_id].variables
        filters = self._trading_methods[method_id].filters
        strategy.backtest(ohlcv, variables=svars, filters=filters, freq=history_id[-1], plot=True)

    backtest_btn.on_click(backtest)
    dropdowns = widgets.HBox([method_dropdown, symbol_dropdown, backtest_btn])
    with option_panel:
        plotly_df(ret)
        display(pd.DataFrame(full_results))

    return widgets.VBox([option_panel, dropdowns, backtest_panel])