見出し画像

【堅実botter向け】Re:ゼロから始めるレンディング生活-利回り3%向上委員会-

こんにちは。今日も元気に仮想通貨botterをやっていますOne(@One619hebotter)です!

この記事にたどり着いている方は仮想通貨とbotの知識が少なからずある前提で、小難しい前置きは無しにして(文才がないのがバレるので)、今回はFTXのレンディングを自動複利にできるソースコードを置いていきたいと思います。詳しいレンディングの仕組みややり方はググってもらえれば無料でかなり詳しい情報が書いてあります。

はじめに

お決まりの呪文を早口で唱えます。

本noteで公開しているロジックおよびソースコードは利益を保証する物ではありません
損失について製作・公開者は一切責任を負いません

本noteでできること

これは言わずもがな、FTXにおけるレンディング収益の向上です。レンディング戦略をbot化することで複利を効かせ、収益向上しようというものです。具体的には、1時間毎に受け取る金利も次の1時間にレンディングに出すというものです。

まず初めにレンディングでどの程度の利回りがあるのでしょうか。現状ではFTXレンディングの利率履歴は見ることができません。必然的に自分で収集するしかないのですが、モノアイさん(@mono_i_love)が収集されたデータがありましたので参考にさせていただいています。かなり有益情報を出してくださるので仮想通貨民はフォロー必須です。

FTXのレンディングは1時間ごとに金利を受け取れるので単利で年間利回りを計算すると、

0.002699% × 24時間 × 365日 ≒ 23.64% 

となります。ではこれを複利換算するとどうなるでしょうか。

(1 + 0.002699%) ** (24時間 × 365日) - 1 ≒ 26.67%

となり約3%の収益向上が期待できます。バブル真っ盛りの仮想通貨界隈からは、え、3%だけ?という声が聞こえてきそうですが、そうです3%だけです。ただし貸出金利というマイナスにならない数字の性質上(日銀のようにマイナス金利にでもなれば話は別ですが)、複利で確実に伸びる部分となりますのでやっておいて損はないかと思います。周りの金融商品を見渡すと利回り数%伸ばすことがどれほど偉大なことか。わざわざレンディングをするかどうかは検討が必要かと思いますが、どうせやるなら複利でやっておきましょう!というnoteです。

ソースコード

それでは早速以下がAPI発注の際のラッパーです。こちらをpyftx.pyとして保存してください。(レンディング部分の動作確認しかしてませんので悪しからず)

import json
import time
from logging import (
   DEBUG, FileHandler, Formatter, NullHandler, StreamHandler, getLogger)

from requests import Request, Session
from requests.exceptions import HTTPError

import hashlib
import hmac
import re
import urllib.parse

from requests.auth import AuthBase


class FTX_Auth(AuthBase):
   def __init__(self, api_key, secret):
       self.api_key = api_key
       self.secret = secret
       
   def __call__(self, r):
       timestamp = str(int(time.time() * 1000))

       o = urllib.parse.urlparse(r.path_url).path
       p = re.sub(r'\A/(public|private)', '', o)
       text = timestamp + r.method + p + (r.body or '')
       sign = hmac.new(self.secret.encode('ascii'), text.encode('ascii'), hashlib.sha256).hexdigest()
       
       headers = dict(r.headers)
       headers['FTX-KEY'] = self.api_key
       headers['FTX-TS'] = timestamp
       headers['FTX-SIGN'] = sign
       r.prepare_headers(headers)
       
       return r

class FTX(object):
   endpoint = 'https://ftx.com/api'

   def __init__(self, api_key, secret, logger=None):
       self.logger = logger or getLogger(__name__)

       self.s = Session()
       self.s.headers['Content-Type'] = 'application/json'
       self.ftx_auth = FTX_Auth(api_key, secret)
   
   def _request(self, method, path, payload, auth):
       for k, v in list(payload.items()):
           if v is None:
               del payload[k]

       if method == 'GET':
           body = None
           query = payload
       else:
           body = json.dumps(payload)
           query = None
       
       if not auth:
           self.s.auth = None
       else:
           self.s.auth = self.ftx_auth

       req = Request(method, self.endpoint + path, data=body, params=query)
       prepped = self.s.prepare_request(req)
       self.logger.debug(f'sending req to {prepped.url}: {prepped.body}')
       
       resp = None
       try:
           resp = self.s.send(prepped)
           resp.raise_for_status()
       except HTTPError as e:
           self.logger.error(e)
       
       self.logger.debug(f'{resp} {resp.text}')

       return resp

   
   # Markets
   """
   This section covers all types of markets on FTX: 
   spot, perpetual futures, expiring futures, and MOVE contracts. 
   Examples for each type are BTC/USD, BTC-PERP, BTC-0626, and BTC-MOVE-1005. 
   For futures that expired in 2019, prepend a 2019 to the date, 
   like so: BTC-20190628 or BTC-MOVE-20190923.
   """
   def get_markets(self):
       payload = {}
       res = self._request('GET', '/markets', payload, auth=False).json()
       
       if not res['success']:
           raise ValueError(f'{res["error"]}')
       return res['result']

   def get_single_market(self, symbol):
       payload = {}
       res = self._request('GET', f'/markets/{symbol}', payload, auth=False).json()
       
       if not res['success']:
           raise ValueError(f'{res["error"]}')
       return res['result']

   def get_orderbook(self, symbol, depth=100):
       payload = {}
       res = self._request('GET', f'/markets/{symbol}/orderbook?depth={depth}', payload, auth=False).json()
       
       if not res['success']:
           raise ValueError(f'{res["error"]}')
       return res['result']

   def get_trades(self, symbol, limit=100):
       payload = {}
       res = self._request('GET', f'/markets/{symbol}/trades?limit={100}', payload, auth=False).json()
       
       if not res['success']:
           raise ValueError(f'{res["error"]}')
       return res['result']

   def get_historical_prices(self, symbol, period, limit=5000):
       payload = {}
       res = self._request('GET', f'/markets/{symbol}/candles?resolution={period}&limit={limit}', payload, auth=False).json()
       
       if not res['success']:
           raise ValueError(f'{res["error"]}')
       return res['result']

   # Futures
   """
   This section covers all types of futures on FTX: 
   perpetual, expiring, and MOVE. Examples for each type are 
   BTC-PERP, BTC-0626, and BTC-MOVE-1005. 
   For futures that expired in 2019, prepend a 2019 to the date, like so: BTC-20190628.
   """

   def list_all_futures(self):
       payload = {}
       res = self._request('GET', '/futures', payload, auth=False).json()
       
       if not res['success']:
           raise ValueError(f'{res["error"]}')
       return res['result']

   def get_future(self, symbol):
       payload = {}
       res = self._request('GET', f'/futures/{symbol}', payload, auth=False).json()
       
       if not res['success']:
           raise ValueError(f'{res["error"]}')
       return res['result']

   def get_future_stats(self, symbol):
       payload = {}
       res = self._request('GET', f'/futures/{symbol}/stats', payload, auth=False).json()
       
       if not res['success']:
           raise ValueError(f'{res["error"]}')
       return res['result']

   def get_funding_rates(self):
       payload = {}
       res = self._request('GET', '/funding_rates', payload, auth=False).json()
       
       if not res['success']:
           raise ValueError(f'{res["error"]}')
       return res['result']

   def get_index_weights(self, index_name):
       payload = {}
       res = self._request('GET', f'/indexes/{index_name}/weights', payload, auth=False).json()
       
       if not res['success']:
           raise ValueError(f'{res["error"]}')
       return res['result']

   def get_expired_futures(self):
       payload = {}
       res = self._request('GET', '/expired_futures', payload, auth=False).json()
       
       if not res['success']:
           raise ValueError(f'{res["error"]}')
       return res['result']

   def get_historical_index(self, symbol, period, limit, start_time, end_time):
       payload = {}
       res = self._request('GET', f'/indexes/{symbol}/candles?resolution={period}&limit={limit}&start_time={start_time}&end_time={end_time}',\
                           payload, auth=False).json()
       
       if not res['success']:
           raise ValueError(f'{res["error"]}')
       return res['result']


   # Account
   def get_account_information(self):
       payload = {}
       res = self._request('GET', '/account', payload, auth=True).json()
       
       if not res['success']:
           raise ValueError(f'{res["error"]}')
       return res['result']

   def get_positions(self):
       payload = {}
       res = self._request('GET', '/positions', payload, auth=True).json()
       
       if not res['success']:
           raise ValueError(f'{res["error"]}')
       return res['result']

   def change_account_leverage(self, leverage):
       payload = {'leverage': leverage}
       res = self._request('POST', '/account/leverage', payload, auth=True).json()
       
       if not res['success']:
           raise ValueError(f'{res["error"]}')
       return res['result']

   # Wallet
   def get_coins(self):
       payload = {}
       res = self._request('GET', '/wallet/coins', payload, auth=True).json()
       
       if not res['success']:
           raise ValueError(f'{res["error"]}')
       return res['result']

   def get_balances(self):
       payload = {}
       res = self._request('GET', '/wallet/balances', payload, auth=True).json()
       
       if not res['success']:
           raise ValueError(f'{res["error"]}')
       return res['result']

   def get_balances_of_all_accounts(self):
       payload = {}
       res = self._request('GET', 'wallet/all_balances', payload, auth=True).json()
       
       if not res['success']:
           raise ValueError(f'{res["error"]}')
       return res['result']

   def get_deposit_address(self, coin, method):
       payload = {}
       res = self._request('GET', f'/wallet/deposit_address/{coin}?method={method}', payload, auth=True).json()
       
       if not res['success']:
           raise ValueError(f'{res["error"]}')
       return res['result']

   def get_deposit_history(self):
       payload = {}
       res = self._request('GET', '/wallet/deposits', payload, auth=True).json()
       
       if not res['success']:
           raise ValueError(f'{res["error"]}')
       return res['result']

   def get_withdrawal_history(self):
       payload = {}
       res = self._request('GET', '/wallet/withdrawals', payload, auth=True).json()
       
       if not res['success']:
           raise ValueError(f'{res["error"]}')
       return res['result']

   def request_withdrawal(self, coin, size, address, tag=None, password=None, code=None):
       payload = {'coin':coin,
                  'size':size,
                  'address':address,
                  'tag':tag,
                  'password':password,
                  'code':code,
                  }
       res = self._request('POST', '/wallet/withdrawals', payload, auth=True).json()
       
       if not res['success']:
           raise ValueError(f'{res["error"]}')
       return res['result']

   def get_airdrops(self):
       payload = {}
       res = self._request('GET', '/wallet/airdrops', payload, auth=True).json()
       
       if not res['success']:
           raise ValueError(f'{res["error"]}')
       return res['result']

   def get_saved_addresses(self):
       payload = {}
       res = self._request('GET', '/wallet/saved_addresses', payload, auth=True).json()
       
       if not res['success']:
           raise ValueError(f'{res["error"]}')
       return res['result']

   def create_saved_addresses(self, coin, address, address_name, isPrimetrust, tag):
       payload = {'coin':coin,
                  'address':address,
                  'addressName':address_name,
                  'isPrimetrust':isPrimetrust,
                  'tag':tag,
                  }
       res = self._request('POST', '/wallet/saved_addresses', payload, auth=True).json()
       
       if not res['success']:
           raise ValueError(f'{res["error"]}')
       return res['result']

   def delete_saved_addresses(self, saved_address_id):
       payload = {'saved_address_id':saved_address_id}
       res = self._request('DELETE', f'/wallet/saved_addresses/{saved_address_id}', payload, auth=True).json()
       
       if not res['success']:
           raise ValueError(f'{res["error"]}')
       return res['result']

   # Orders
   def get_open_orders(self, symbol):
       payload = {}
       res = self._request('GET', f'/orders?market={symbol}', payload, auth=True).json()
       
       if not res['success']:
           raise ValueError(f'{res["error"]}')
       return res['result']

   def get_order_history(self, symbol):
       payload = {}
       res = self._request('GET', f'/orders/history?market={symbol}', payload, auth=True).json()
       
       if not res['success']:
           raise ValueError(f'{res["error"]}')
       return res['result']

   def get_open_trigger_orders(self, symbol):
       payload = {}
       res = self._request('GET', f'/conditional_orders?market={symbol}', payload, auth=True).json()
       
       if not res['success']:
           raise ValueError(f'{res["error"]}')
       return res['result']

   def get_trigger_order_triggers(self, order_id):
       payload = {}
       res = self._request('GET', f'/conditional_orders/{order_id}/triggers', payload, auth=True).json()
       
       if not res['success']:
           raise ValueError(f'{res["error"]}')
       return res['result']

   def get_trigger_order_history(self, symbol):
       payload = {}
       res = self._request('GET', f'/conditional_orders/history?market={symbol}', payload, auth=True).json()
       
       if not res['success']:
           raise ValueError(f'{res["error"]}')
       return res['result']

   def place_order(self, symbol, side, price, order_type, size, reduce_only=False, ioc=False, post_only=False, client_id=None):
       payload = {'market':symbol,
                  'side':side,
                  'price':price,
                  'type':order_type,
                  'size':size,
                  'reduceOnly':reduce_only,
                  'ioc':ioc,
                  'postOnly':post_only,
                  'clientId':client_id,
                  }
       res = self._request('POST', '/orders', payload, auth=True).json()
       
       if not res['success']:
           raise ValueError(f'{res["error"]}')
       return res['result']

   def place_trigger_order(self, symbol, side, price, order_type, size, reduce_only=False, retry_until_filled=False):
       payload = {'market':symbol,
                  'side':side,
                  'size':size,
                  'type':order_type,
                  'reduceOnly':reduce_only,
                  'retryUntilFilled':retry_until_filled,
                  }
       res = self._request('POST', '/conditional_orders', payload, auth=True).json()
       
       if not res['success']:
           raise ValueError(f'{res["error"]}')
       return res['result']

   def modify_order(self, order_id, price, size, client_id=None):
       payload = {'price':price,
                  'size':size,
                  'clientId':client_id,
                  }
       res = self._request('POST', f'/orders/{order_id}/modify', payload, auth=True).json()
       
       if not res['success']:
           raise ValueError(f'{res["error"]}')
       return res['result']

   def modify_order_by_client_id(self, client_order_id, price, size, client_id=None):
       payload = {'price':price,
                  'size':size,
                  'clientId':client_id,
                  }
       res = self._request('POST', f'/orders/by_client_id/{client_order_id}/modify', payload, auth=True).json()
       
       if not res['success']:
           raise ValueError(f'{res["error"]}')
       return res['result']

   def modify_trigger_order(self, order_id, size, trigger_price, order_price):
       payload = {'size':size,
                  'triggerPrice':trigger_price,
                  'orderPrice':order_price,
                  }
       res = self._request('POST', f'/conditional_orders/{order_id}/modify', payload, auth=True).json()
       
       if not res['success']:
           raise ValueError(f'{res["error"]}')
       return res['result']

   def get_order_status(self, order_id):
       payload = {}
       res = self._request('GET', f'/orders/{order_id}', payload, auth=True).json()
       
       if not res['success']:
           raise ValueError(f'{res["error"]}')
       return res['result']

   def get_order_status_by_client_id(self, client_order_id):
       payload = {}
       res = self._request('GET', f'/orders/by_client_id/{client_order_id}', payload, auth=True).json()
       
       if not res['success']:
           raise ValueError(f'{res["error"]}')
       return res['result']

   def cancel_order(self, order_id):
       payload = {}
       res = self._request('DELETE', f'/orders/{order_id}', payload, auth=True).json()
       
       if not res['success']:
           raise ValueError(f'{res["error"]}')
       return res['result']

   def cancel_order_by_client_id(self, client_order_id):
       payload = {}
       res = self._request('DELETE', f'/orders/by_client_id/{client_order_id}', payload, auth=True).json()
       
       if not res['success']:
           raise ValueError(f'{res["error"]}')
       return res['result']

   def cancel_open_trigger_order(self, id):
       payload = {}
       res = self._request('DELETE', f'/conditional_orders/{id}', payload, auth=True).json()
       
       if not res['success']:
           raise ValueError(f'{res["error"]}')
       return res['result']

   def cancel_all_orders(self, symbol, conditional_orders_only=False, limit_orders_only=False):
       payload = {'market':symbol,
                  'conditionalOrdersOnly':conditional_orders_only,
                  'limitOrdersOnly':limit_orders_only,
                  }
       res = self._request('DELETE', '/orders', payload, auth=True).json()
       
       if not res['success']:
           raise ValueError(f'{res["error"]}')
       return res['result']

   # Convert
   def request_quote(self, fromcoin, tocoin, size):
       payload = {'fromCoin':fromcoin,
                  'toCoin':tocoin,
                  'size':size,
                  }
       res = self._request('POST', '/otc/quotes', payload, auth=True).json()
       
       if not res['success']:
           raise ValueError(f'{res["error"]}')
       return res['result']

   def get_quote_status(self, quote_id):
       payload = {}
       res = self._request('GET', f'/otc/quotes/{quote_id}', payload, auth=True).json()
       
       if not res['success']:
           raise ValueError(f'{res["error"]}')
       return res['result']

   def accept_quote(self, quote_id):
       payload = {}
       res = self._request('POST', f'/otc/quotes/{quote_id}/accept', payload, auth=True).json()
       
       if not res['success']:
           raise ValueError(f'{res["error"]}')
       return res['result']

   # Spot Margin
   def get_borrow_rates(self):
       payload = {}
       res = self._request('GET', '/spot_margin/borrow_rates', payload, auth=True).json()
       
       if not res['success']:
           raise ValueError(f'{res["error"]}')
       return res['result']

   def get_lending_rates(self):
       payload = {}
       res = self._request('GET', '/spot_margin/lending_rates', payload, auth=True).json()
       
       if not res['success']:
           raise ValueError(f'{res["error"]}')
       return res['result']

   def get_daily_borrowed_amounts(self):
       payload = {}
       res = self._request('GET', '/spot_margin/borrow_summary', payload, auth=True).json()
       
       if not res['success']:
           raise ValueError(f'{res["error"]}')
       return res['result']

   def get_market_info(self, symbol):
       payload = {}
       res = self._request('GET', f'/spot_margin/market_info?market={symbol}', payload, auth=True).json()
       
       if not res['success']:
           raise ValueError(f'{res["error"]}')
       return res['result']

   def get_borrow_history(self):
       payload = {}
       res = self._request('GET', '/spot_margin/borrow_history', payload, auth=True).json()
       
       if not res['success']:
           raise ValueError(f'{res["error"]}')
       return res['result']

   def get_lending_history(self):
       payload = {}
       res = self._request('GET', '/spot_margin/lending_history', payload, auth=True).json()
       
       if not res['success']:
           raise ValueError(f'{res["error"]}')
       return res['result']

   def get_lending_offers(self):
       payload = {}
       res = self._request('GET', '/spot_margin/offers', payload, auth=True).json()
       
       if not res['success']:
           raise ValueError(f'{res["error"]}')
       return res['result']

   def get_lending_info(self):
       payload = {}
       res = self._request('GET', '/spot_margin/lending_info', payload, auth=True).json()
       
       if not res['success']:
           raise ValueError(f'{res["error"]}')
       return res['result']

   def submit_lending_offer(self, coin, size, rate):
       payload = {'coin': coin,
                  'size': size,
                  'rate': rate
                  }
       res = self._request('POST', '/spot_margin/offers', payload, auth=True).json()
       
       if not res['success']:
           raise ValueError(f'{res["error"]}')
       return res['result']

   # Fills
   def get_fills(self, symbol):
       payload = {}
       res = self._request('GET', f'/fills?market={symbol}', payload, auth=True).json()
       
       if not res['success']:
           raise ValueError(f'{res["error"]}')
       return res['result']

   # Funding Payments
   def get_funding_payments(self):
       payload = {}
       res = self._request('GET', '/funding_payments', payload, auth=True).json()
       
       if not res['success']:
           raise ValueError(f'{res["error"]}')
       return res['result']

   # Leveraged Tokens
   def list_leveraged_tokens(self):
       payload = {}
       res = self._request('GET', '/lt/tokens', payload, auth=True).json()
       
       if not res['success']:
           raise ValueError(f'{res["error"]}')
       return res['result']

   def get_token_info(self, token_name):
       payload = {}
       res = self._request('GET', f'/lt/{token_name}', payload, auth=True).json()
       
       if not res['success']:
           raise ValueError(f'{res["error"]}')
       return res['result']

   def get_leveraged_token_balances(self):
       payload = {}
       res = self._request('GET', '/lt/balances', payload, auth=True).json()
       
       if not res['success']:
           raise ValueError(f'{res["error"]}')
       return res['result']

   def list_leveraged_token_creation_requests(self):
       payload = {}
       res = self._request('GET', '/lt/creations', payload, auth=True).json()
       
       if not res['success']:
           raise ValueError(f'{res["error"]}')
       return res['result']

   def request_leveraged_token_creation(self, token_name):
       payload = {}
       res = self._request('POST', f'/lt/{token_name}/create', payload, auth=True).json()
       
       if not res['success']:
           raise ValueError(f'{res["error"]}')
       return res['result']

   def list_leveraged_token_redemption_requests(self):
       payload = {}
       res = self._request('GET', '/lt/redemptions', payload, auth=True).json()
       
       if not res['success']:
           raise ValueError(f'{res["error"]}')
       return res['result']

   def request_leveraged_token_redemption(self, token_name):
       payload = {}
       res = self._request('POST', f'/lt/{token_name}/redeem', payload, auth=True).json()
       
       if not res['success']:
           raise ValueError(f'{res["error"]}')
       return res['result']

そして次に以下がメインのコードftx_lending.pyとなります。仮想通貨botではあまり使われていないscheduleというライブラリを使っていますのでpip install scheduleで対応をお願いします。できるだけシンプルに記述したつもりですが記述ミスありましたらご指摘いただけるとありがたいです。

# -*- coding: utf-8 -*-
from time import sleep
from datetime import datetime
import pyftx
date_format = "%Y/%m/%d-%H:%M:%S"


class FTX(object):    
   def __init__(self, api_key, secret, coin='USD', min_rate=0.00001):
       self.ftx = pyftx.FTX(api_key=api_key, secret=secret)
       self.coin = coin
       self.min_rate = min_rate

   def rebalance(self):
       failed = 0
       while True:
           try:
               balance = 0
               data = self.ftx.get_balances()
               for i in data:
                   if i['coin'] == self.coin:
                       balance += i['total']
               
               self.ftx.submit_lending_offer(self.coin, balance, self.min_rate)
               print(f"{datetime.now().strftime(date_format)}\nsubmitted size: {balance}\n\n")
               return
               
           except Exception as e:
               print(e)
               failed += 1
               if failed > 3:
                   return
               else:
                   sleep(10)
           
if __name__ == '__main__':
   import schedule
   # apikeyを入力してください
   YOUR_APIKEY = ''
   # apisecretを入力してください
   YOUR_SECRET = ''

   # coinはデフォルトでUSD貸出としています
   # min_rateは年利8.76%以上で貸出にしていますが他に使い道が無ければ0で良いかと思います
   ex = FTX(api_key=YOUR_APIKEY, secret=YOUR_SECRET, coin='USD', min_rate=0.00001)
   ex.rebalance()
   # webで見たところ金利の受け取りに少しラグがある(毎時:05~10分ごろに反映?)ので念のため20分にオファーを提出しています
   # 毎時20分に受け取った金利も含めたすべてのUSDを貸し出します
   for i in range(24):
       if i < 10:
           num = '0' + str(i)
       else:
           num = i
       schedule.every().day.at(f"{num}:20").do(ex.rebalance)
   
   while True:
       schedule.run_pending()
       sleep(10)
       

ftx_lending.pyをそのまま実行していただければ動くようになっています。

おわりに

絶好調の仮想通貨界隈で今後の見通しも明るいとされています。しかし予測不能の事態というのはその名の通り予測不能なので、今できることを精一杯して備えるべきではないかと思います。私自身もこういった小さい積み重ねをたくさんしており、そういった積み重ねがあるからこそ攻めるべきところでしっかり攻めるメンタルを保つことができています。本noteが堅実なbotterの一助となれば幸いです。