Ejemplo n.º 1
0
class Balance_Manager():

    logger = logging.getLogger()

    def __init__(self, address, web3, dss, ilk, gem_join, vat_target, max_gem_balance, max_gem_sale, gem_eth_ratio, profit_margin, tab_discount, bid_start_time):
        self.our_address = address
        self.web3 = web3
        self.dss = dss
        self.dsr = Dsr(self.dss, self.our_address)
        self.ilk = ilk
        self.gem_join = gem_join
        self.vat_target = vat_target
        self.max_gem_balance = max_gem_balance #max gem (ETH, BAT) balance for keeper
        self.max_gem_sale = max_gem_sale #max lot to sell in single transaction to avoid slippage
        self.gem_eth_ratio = gem_eth_ratio
        self.profit_margin = profit_margin
        self.bid_start_time = bid_start_time
        self.start_time = time.time()
        self.round_trip_gas = 950000 #total gas to witdraw, bid, sell, redeposit DAI from DSR
        self.user_proxy = None
        self.dsr_balance = None
        self.vat_balance = None
        self.auction_tab = {}
        self.time_log = {}
        self.log30 = {}
        self.log5 = {}
        self.bid_checker = {}
        self.auc_withdraw = {}
        self.tab_discount = tab_discount
        self.high_threshold = tab_discount[0]
        self.high_discount = tab_discount[1]
        self.low_threshold = tab_discount[2]
        self.low_discount = tab_discount[3]
        self.saving = False
        self.unloading = False

    def startup(self, gasprice):
        logging.info(f"")
        logging.info(f"***** WELCOME TO THE THRIFTY KEEPER *****")
        self.dsr_approve(gasprice)
        self.threader('unload', gasprice)
        self.log_balances()
        self.report_margin()
        logging.info(f"")
    
    def threader(self, func, gasprice):
        if func is 'unload':
            vat_gem_balance = self.get_vat_gem_balance()
            if vat_gem_balance > Wad.from_number(self.max_gem_balance) and not self.unloading:
                self.unloading = True
                threading.Thread(target=self.unload, args=(gasprice, vat_gem_balance),daemon=False).start()
            else:
                return
        elif func is 'save':
            time_elapse = time.time()-self.start_time
            if time_elapse < 300: #dont withdraw on startup as may be auctions running
                return
            act_dai_balance = self.get_dai_balance()
            vat_dai_balance = self.get_vat_balance()
            if (vat_dai_balance == Wad(0) and act_dai_balance == Wad(0)) or self.saving: 
                return
            else:
                self.saving = True
                threading.Thread(target=self.save, args=(gasprice, vat_dai_balance),daemon=False).start()
                 

    def unload(self, gasprice, vat_gem_balance):
        self.check_gas_station(gasprice)
        self.withdraw_gem(gasprice, vat_gem_balance)
        self.unwrap_weth(gasprice)
        self.sell_gem_for_dai()
        self.unloading = False
    
    def save(self, gasprice, vat_dai_balance):
        self.vat_withdraw(gasprice, vat_dai_balance)
        self.dsr_add(gasprice)
        self.saving = False
    
    def check_gas_station(self, gasprice):
        while True:
            if gasprice.gas_station is None:
                break
            elif gasprice.gas_station._fast_price:
                break
            else:
                time.sleep(1)
    
    def withdraw_gem(self, gasprice, vat_gem_balance):
        self.logger.info(f"Exiting {round(vat_gem_balance.__float__(),3)} {self.ilk.name} from the Vat")
        self.gem_join.exit(self.our_address, vat_gem_balance).transact(gas_price=gasprice)
       
        
    def unwrap_weth(self, gas_price):
        weth_address = Address(abis.weth_address)
        weth_contract = self.web3.eth.contract(address=abis.weth_address, abi=abis.weth_abi)
        weth_balance = weth_contract.functions.balanceOf(self.our_address.address).call()
        if weth_balance > 0:
            withdraw = Transact(self, self.web3, abis.weth_abi, weth_address, weth_contract, 'withdraw', [weth_balance])
            withdraw.transact (gas_price=gas_price)
            time.sleep(1)
            eth_balance = self.web3.eth.getBalance(self.our_address.address)
            self.logger.info(f"New ETH balance: {eth_balance/1e18}")
        else:
            logging.info(f"No WETH to unwrap")
    
    def report_margin(self):
        if self.high_threshold < self.low_threshold: #user entry check transpose
            x = self.high_threshold
            y = self.high_discount
            self.high_threshold = self.low_threshold
            self.low_threshold = x
            self.high_discount = self.low_discount
            self.low_discount = y
    
        high_threshold = "{:,}".format(int(self.high_threshold))
        low_threshold = "{:,}".format(int(self.low_threshold))
        logging.info(f"Base bid profit margin = {round(self.profit_margin*100, 3)}%")
        logging.info(f"Bid profit margin = {round((self.profit_margin + self.low_discount)*100, 3)}% when the sum of all active auction tabs is > {low_threshold} DAI")
        logging.info(f"Bid profit margin = {round((self.profit_margin + self.high_discount)*100, 3)}% when the sum of all active auction tabs is > {high_threshold} DAI")
        logging.info(f"If you win an auction, the collateral will be sold until there is {self.max_gem_balance} {self.ilk.name} remaining in your keeper account ")
        logging.info(f"Max gem sale amount in a single transaction is {self.max_gem_sale} {self.ilk.name}")
        logging.info(f"You will not submit bids until there are less than {self.bid_start_time}m left in the auction")
        logging.info(f"*****")
        
    def log_balances(self):
        dai_balance = self.get_dai_balance().__float__()
        dsr_balance = self.get_dsr_balance().__float__()
        eth_balance = self.web3.eth.getBalance(self.our_address.address)/1e18
        vat_balance = self.get_vat_balance().__float__()
        vat_gem_balance = self.get_vat_gem_balance().__float__()
        vat_target = self.vat_target.__float__()
        self.logger.info(f"Keeper Dai Balance =  {round(dai_balance,2)}")
        self.logger.info(f"Keeper DSR Balance = {round(dsr_balance,1)}")
        logging.info(f"Keeper ETH Balance = {round(eth_balance, 3)}")
        self.logger.info(f"Vat DAI Balance = {round(vat_balance,3)}")
        logging.info(f"Vat {self.ilk.name} Balance = {round(vat_gem_balance, 3)}")
        self.logger.info(f"Vat DAI Target = {round(vat_target,1)}")

    def get_dai_balance(self):
        return Wad(self.dsr.mcd.dai.balance_of(self.our_address))
    
    def get_dsr_balance(self):
        if self.dsr.has_proxy():
            user_proxy = self.dsr.get_proxy()
            return self.dsr.get_balance(user_proxy.address)
        else:
            return None

    def get_vat_balance(self):
        return Wad(self.dss.vat.dai(self.our_address))
    
    def get_vat_gem_balance(self):
        return Wad(self.dss.vat.gem(self.ilk, self.our_address))

    def add_tab(self):
        x = Rad(0)
        for value in self.auction_tab.values():
            x = x + value
        return x
    
    def get_tab_discount(self):
        tot_tab = self.add_tab()
        if tot_tab > Rad.from_number(self.high_threshold):
            discount = self.high_discount
        elif tot_tab > Rad.from_number(self.low_threshold):
            discount = self.low_discount
        else:
            discount = 0
        return(discount)
    
    def register_tab(self, auction_id, tab):
        if auction_id not in self.auction_tab.keys():
            self.auction_tab[auction_id] = tab
            tot_tab = self.add_tab()
            t = round(tab.__float__(), 3)
            self.logger.info(f"Registering auction {auction_id} with tab of {t} DAI.  Total of all active tabs is now {round(tot_tab.__float__(), 3)} DAI")
        

    
    def analyze_profit(self, gas_price, feed_price, lot, end, current_price, beg, auction_id):
        #gas estimates:
        #dsr_withdraw = 160000
        #vat_deposit = 50500
        #bid = 93000
        #deal = 52000
        #exit_weth = 59500
        #unwrap = 25000
        #sell_eth = 250000 x 2
        #vat withdraw = 80000
        #dsr_deposit = 160000

        def log_auction_stats():
            tot_tab = self.add_tab()
            num_auctions = len(self.auction_tab.keys())
            tab_discount = self.get_tab_discount()
            time_to_bid = int((time_left/60)-self.bid_start_time)
            logging.info(f"*****")
            self.logger.info(f"Auction id: {auction_id}")
            logging.info(f"Time left: {int(time_left/60)}m")
            logging.info(f"Auction tab: {round(self.auction_tab[auction_id].__float__(),3)} DAI")
            logging.info(f"Total of {num_auctions} active auctions for {round(tot_tab.__float__(), 3)} DAI. Tab discount: {tab_discount*100}%")
            logging.info(f"Lot size: {round(lot,4)} {self.ilk.name}")
            self.logger.info(f"Gas cost: {round(gas_cost_dai, 3)} DAI")
            self.logger.info (f"Market feed price: {round(feed_price, 2)} {self.ilk.name}/DAI")
            self.logger.info(f"Current bid price: {round(current_price, 2)} {self.ilk.name}/DAI")
            self.logger.info(f"Your profit price: {round(profit_price_daieth, 2)} {self.ilk.name}/DAI")
            self.logger.info(f"Current bid profit margin: {round(margin, 2)} %")
            logging.info(f"Your min profit margin: {round(((self.profit_margin+tab_discount)*100), 2)}% ")
            if profit_price_daieth > current_price:
                self.logger.info(f"Looks profitable to bid")
                if bidable:
                    logging.info(f"Your profit price is above the min bid increment.")
                    logging.info(f"Min new bid = {round(current_price*beg, 2)}")
                else:
                    logging.info(f"Your profit price is below the min bid increment.")
                    logging.info(f"Min new bid = {round(current_price*beg, 2)}")
            else:
                logging.info(f"Doesn't look profitable to bid.")
            if time_left > self.bid_start_time*60:
                logging.info(f"Too much time left. Will consider bidding in {time_to_bid} minutes.")
                logging.info ("Leaving your DAI in the DSR for now")
            logging.info(f"*****")

        def calc_margin():

            #Estimate gas costs in dai of exiting DSR, bidding, selling, redeposit
            gp = gas_price.get_gas_price(1) #fast gas price
            if self.ilk.name == 'BAT-A':
                gas_price_eth = feed_price * self.gem_eth_ratio
            else:
                gas_price_eth = feed_price
            gas_cost_dai = self.round_trip_gas * gas_price_eth * gp * 1e-18
            
            #Determine profit margin to discount feed_price
            tab_discount = self.get_tab_discount() #additional discount when large CDPs are being auctioned
            target_margin = self.profit_margin + tab_discount
            
            #Gross revenue from sale of dai at market price
            sell_amt_dai = (lot * feed_price)
            #Auction cost in dai at currently bid price
            bid_dai = (lot * current_price) + gas_cost_dai
            #Profit margin of current bid
            margin =  (sell_amt_dai - bid_dai)/bid_dai * 100
            #Price per gem that provides target profit margin after gas costs 
            profit_price_daieth = (sell_amt_dai-((1+target_margin)*gas_cost_dai))/((1+target_margin)*lot)
            
            if profit_price_daieth > current_price * beg:
                bidable = True
            else:
                bidable = False
            return(profit_price_daieth, margin, gas_cost_dai, bidable)

        def check_new_bid(current_price, id):
            if id in self.bid_checker.keys():
                prior_bid = self.bid_checker[id]
            else:
                self.bid_checker[id] = current_price
                return False
            new_bid = current_price
            if new_bid == prior_bid:
                return False
            elif new_bid > prior_bid:
                self.bid_checker[id] = new_bid
                return True
        
        try:
            feed_price = feed_price.__float__()
            current_price = current_price.__float__()
            lot = lot.__float__()
            beg = beg.__float__()
            time_left = end - time.time()
            new_bid = check_new_bid(current_price, id)

            (profit_price_daieth, margin, gas_cost_dai, bidable) = calc_margin()

            if new_bid:
                logging.info(f"New bid detected")
                log_auction_stats()

            if time_left > (self.bid_start_time * 60):
                if auction_id not in self.time_log.keys():
                    logging.info(f"Initial auction stats")
                    self.time_log[auction_id] = True
                    log_auction_stats()
                return None
        
            if time_left <= (self.bid_start_time * 60) and not bidable and auction_id not in self.log30.keys():
                logging.info(f"Open for bidding")
                self.log30[auction_id] = True
                log_auction_stats()
            
            elif time_left <= 300 and not bidable and auction_id not in self.log5.keys():
                logging.info(f"5 min left")
                self.log5[auction_id] = True
                log_auction_stats()
            
            if bidable:
                logging.info(f"Bidable")
                log_auction_stats()
            
            if profit_price_daieth > current_price and bidable:
                return Wad.from_number(profit_price_daieth)
            else:
                return None
            
        except Exception as e:
            self.logger.info(f"{e}")
            return None


    def sell_gem_for_dai(self):

        token = self.ilk.name.split('-')

        def get0x(sell_amount):
            parameters = {
            'buyToken' : 'DAI',
            'sellToken' : token[0],
            'sellAmount' : str(int(sell_amount))
            }
            url = 'https://api.0x.org/swap/v0/quote'
            response = requests.get(url, params=parameters)
            if response.status_code == 200:
                rawdict = response.json()
                txdict = {}
                txdict['nonce'] = self.web3.eth.getTransactionCount(self.our_address.address)
                txdict['to'] = self.web3.toChecksumAddress(rawdict['to'])
                txdict['value'] = int(rawdict['value'])
                txdict['gas'] = int(rawdict['gas']) + 300000
                txdict['gasPrice'] = int(rawdict['gasPrice'])
                txdict['data']= rawdict['data']
                return txdict
            else:
                return False
            
        def determine_sale_info(balance):
            total_sell = (balance - self.max_gem_balance)/1e18
            if total_sell > self.max_gem_sale:
                num_sales = int(total_sell/self.max_gem_sale) + 1
            else:
                num_sales = 1
            sale_amount = round(total_sell/num_sales)*1e18
            return (sale_amount, num_sales)
        
        def get_act_balance():
            if token[0] == 'BAT':
                bat_contract = self.web3.eth.contract(address=abis.bat_address, abi=abis.bat_abi)
                return bat_contract.functions.balanceOf(self.our_address.address).call()
            elif token[0] == 'ETH':
                return self.web3.eth.getBalance(self.our_address.address)
        z=True
        while z == True:
            logging.info(f"{z}")
            balance = get_act_balance()
            dai_balance = self.get_dai_balance()
            self.logger.info(f"{token[0]} balance: {round(balance/1e18,3)}")
            if balance > self.web3.toWei(self.max_gem_balance, 'ether'):
                (sell_amount, num_sales) = determine_sale_info(balance)
                tot_sale = round((balance/1e18 - self.max_gem_balance), 3)
                self.logger.info(f"Selling {tot_sale} {token[0]} for DAI to maintain {self.max_gem_balance} {token[0]} in account")
                for x in range (num_sales):
                    self.logger.info(f"Checking 0x API")
                    txdict = get0x(sell_amount)
                    if txdict:
                        self.logger.info(f"Selling {round(sell_amount/1e18,3)} {token[0]}")
                        txhash = self.web3.eth.sendTransaction(txdict)
                        self.web3.eth.waitForTransactionReceipt(txhash)
                        new_dai_balance = self.get_dai_balance()
                        if dai_balance == new_dai_balance:
                            logging.info ("Transaction failed")
                            z=False
                            break
                        else:
                            self.logger.info(f"Done")
                            if (num_sales - x) > 1:
                                time.sleep(60)
                    else:
                        time.sleep(1)
                        self.logger.info(f"0x down")
                self.log_balances()
            else:
                self.logger.info(f"Keeper balance <= {self.max_gem_balance} {token[0]}")
                break
        
    
    def dsr_approve(self, gas_price):
         # Checking if the user has a DS-Proxy - if not, we build one.
        if self.dsr.has_proxy() == False:
            self.logger.info(f"No DS-Proxy found - Building new proxy...")
            self.dsr.build_proxy().transact(gas_price = gas_price)
            time.sleep(1)
            self.user_proxy = self.dsr.get_proxy()
            self.logger.info(f"Built new proxy at: {self.user_proxy.address.address}")

            # Approving the DS-Proxy to move Dai from our wallet to the DSR
            logging.info(f"Approving account to spend Dai...")
            self.dsr.mcd.dai.approve(self.user_proxy.address.address).transact(gas_price=gas_price)
            self.logger.info(f"Approved DS-Proxy to spend Dai. Keeper's DSR is now configured")

        if self.dsr.has_proxy() == True:
            self.user_proxy = self.dsr.get_proxy()
            self.logger.info(f"Keeper's DSR is configured. Existing DS-Proxy found at: {self.user_proxy.address.address}")
    
    def dsr_add(self, gas_price):
        # Adding Dai to the DSR
        dai_balance = self.get_dai_balance()
        if dai_balance > Wad(0):
            self.logger.info("Adding Dai balance to the DSR")
            self.dsr.join(dai_balance, self.user_proxy).transact(gas_price=gas_price)
            self.log_balances()

    def dsr_withdraw(self, gas_price, id):
        '''withdraw vat_targer dai from dsr'''

        amt = Wad(self.add_tab())

        if id in self.auc_withdraw.keys(): #prevent new withdraws after bidding
            return False

        if not self.get_dsr_balance() > Wad(0):
            self.logger.debug(f"No Dai in the DSR")
            return False
        
        elif amt <= self.get_vat_balance():
            vat_b = self.get_vat_balance()
            self.logger.debug(f"vat balance = {vat_b}")
            self.logger.debug(f"Enough Dai in the Vat")
            return False

        else:
            
            max_add = self.vat_target - self.get_vat_balance()
            need_to_bid = amt - self.get_vat_balance()

            tot = need_to_bid + Wad.from_number(1) 

            if need_to_bid > max_add:
                tot = max_add + Wad.from_number(1) 

            if need_to_bid > self.get_dsr_balance() and self.get_tab_discount() < self.high_discount:
                return False #don't waste gas to bid unless above high tab limit
            elif need_to_bid > self.get_dsr_balance() and self.get_tab_discount() == self.high_discount:
                tot = self.get_dsr_balance()          
            
            if tot < Wad.from_number(.001): #avoid withdraw due to rounding errors
                return False
            self.logger.info(f"Withdrawing {tot} Dai from DSR")
            self.dsr.exit(tot, self.user_proxy).transact(gas_price=gas_price)
            self.auc_withdraw[id] = True
            time.sleep(2)
            self.log_balances()
            return True
    
    def vat_withdraw(self, gas_price, vat_dai_balance):
        if vat_dai_balance > 0:
            self.logger.info(f"Exiting {round(vat_dai_balance.__float__(), 3)} Dai from the Vat to deposit in the DSR")
            self.dss.dai_adapter.exit(self.our_address, vat_dai_balance).transact(gas_price=gas_price)
            time.sleep(2)
            self.log_balances()
    
    def remove_auction (self,id):
        if id in self.auction_tab.keys():
            del self.auction_tab[id]
        if id in self.bid_checker.keys():
            del self.bid_checker[id]
        if id in self.auc_withdraw.keys():
            del self.auc_withdraw[id]
Ejemplo n.º 2
0
class DsrProxyDemo:
    """ DSR Python Integration Example using DSProxy
    """
    # _DAI_AMOUNT is the amount of Dai we are adding to the DSR in this demo. Set to 1 Dai as standard.
    _DAI_AMOUNT = Wad.from_number(1)
    _USER_PROXY = None

    def __init__(self, args, **kwargs):
        parser = argparse.ArgumentParser("dsrdemo")

        parser.add_argument(
            "--eth-from",
            type=str,
            required=True,
            help=
            "Ethereum address from which to send transactions; checksummed (e.g. '0x12AebC')"
        )

        parser.add_argument("--rpc-host",
                            type=str,
                            default="localhost",
                            help="JSON-RPC host (default: `localhost')")

        parser.add_argument(
            "--network",
            type=str,
            required=True,
            help=
            "Network that you're running the Keeper on (options, 'mainnet', 'kovan', 'testnet')"
        )

        parser.add_argument("--rpc-port",
                            type=int,
                            default=8545,
                            help="JSON-RPC port (default: `8545')")

        parser.add_argument("--rpc-timeout",
                            type=int,
                            default=10,
                            help="JSON-RPC timeout (in seconds, default: 10)")

        parser.add_argument(
            "--eth-key",
            type=str,
            nargs='*',
            help=
            "Ethereum private key(s) to use (e.g. 'key_file=/path/to/keystore.json,pass_file=/path/to/passphrase.txt')"
        )
        self.arguments = parser.parse_args(args)

        self.web3 = kwargs['web3'] if 'web3' in kwargs else Web3(
            HTTPProvider(
                endpoint_uri=
                f"https://{self.arguments.rpc_host}:{self.arguments.rpc_port}",
                request_kwargs={"timeout": self.arguments.rpc_timeout}))
        self.web3.eth.defaultAccount = self.arguments.eth_from
        register_keys(self.web3, self.arguments.eth_key)
        self.our_address = Address(self.arguments.eth_from)

        # Instantiate the dss and dsr classes
        self.dss = DssDeployment.from_network(web3=self.web3,
                                              network=self.arguments.network)
        self.dsr = Dsr(self.dss, self.our_address)

    def main(self):
        # Checking if the user has a DS-Proxy - if not, we build one.
        if self.dsr.has_proxy() == False:
            print("No DS-Proxy found - Building new proxy...")
            self.dsr.build_proxy().transact()
            print("Built new proxy at: " +
                  self.dsr.get_proxy().address.address)

        if self.dsr.has_proxy() == True:
            self._USER_PROXY = self.dsr.get_proxy()
            print("Existing DS-Proxy found at: " +
                  self.dsr.get_proxy().address.address)

        # Saving the User Proxy in a variable
        self._USER_PROXY = self.dsr.get_proxy()

        # Saving the initial Dai balance of the user for calculations further down
        self.initialDaiBalance = self.dsr.mcd.dai.balance_of(self.our_address)

        # Approving the DS-Proxy to move Dai from our wallet to the DSR
        self.approve()
        print("Approved DS-Proxy to spend Dai")

        # Adding Dai to the DSR - Amount specified nby _DAI_AMOUNT variable.
        self.addDaiToDsr()
        print(f"Added      {self._DAI_AMOUNT} Dai to DSR")

        # Calculating the balance of our Dai in DSR.
        self.DsrBalance = self.dsr.get_balance(self._USER_PROXY.address)

        # Note: if the DSR is 0%, the resulting Dai balance may be 1 wei less
        # than what deposited (due to rounding)
        print(f"Wait 1 minute for Dai to accrue DSR proceeds")
        time.sleep(60)

        # Retrieving all Dai from DSR.
        self.exitAllDaiFromDsr()

        # Calculating how much Dai you have earned, by checking the difference between the initial and final Dai balance
        self.finalDaiBalance = self.dsr.mcd.dai.balance_of(self.our_address)
        self.balanceDifference = self.finalDaiBalance - self.initialDaiBalance

        print(
            f"Retrieved  {self.balanceDifference + self._DAI_AMOUNT} Dai from DSR"
        )  # We are adding the amount of Dai you added to DSR to get the full amount retrieved.
        print(f"You earned {self.balanceDifference} Dai")

    def approve(self):
        self.dsr.mcd.dai.approve(self._USER_PROXY.address).transact()

    def addDaiToDsr(self):
        self.dsr.join(self._DAI_AMOUNT, self._USER_PROXY).transact()

    def exitDaiFromDsr(self, dai: Wad):
        self.dsr.exit(dai, self._USER_PROXY).transact()

    def exitAllDaiFromDsr(self):
        self.dsr.exit_all(self._USER_PROXY).transact()
Ejemplo n.º 3
0
print(our_address)

dsr_client = Dsr(mcd, our_address)

print(f"Chi: {dsr_client.chi()}")
print(f"Total DAI: {dsr_client.get_total_dai()}")
print(f"DSR: {dsr_client.dsr()}")

proxy = dsr_client.get_proxy()
print(f"Has Proxy: {dsr_client.has_proxy()}")

if not dsr_client.has_proxy():
    dsr_client.build_proxy().transact()

proxy = dsr_client.get_proxy()
print(f"Proxy address: {proxy.address.address}")

print(f"Balance: {dsr_client.get_balance(proxy.address)}")

# approve proxy to use 10 DAI from account
dsr_client.mcd.dai.approve(proxy.address, Wad.from_number(10)).transact()

dsr_client.join(Wad.from_number(2.2), proxy).transact()
print(f"Balance: {dsr_client.get_balance(proxy.address)}")

dsr_client.exit(Wad.from_number(1.01), proxy).transact()
print(f"Balance: {dsr_client.get_balance(proxy.address)}")

dsr_client.exit_all(proxy).transact()
print(f"Balance: {dsr_client.get_balance(proxy.address)}")