class Coinbase: def __init__(self, config): # TODO command-line parameters? # https://docs.python.org/3/library/argparse.html account = input("Account: ") if account == "Kevin" or "kevin" or "k": api_key = config["coinbase_api_kevin"]["key"] api_secret = config["coinbase_api_kevin"]["secret"] elif account == "Liang" or "liang" or "l": api_key = config["coinbase_api_liang"]["key"] api_secret = config["coinbase_api_liang"]["secret"] else: print("Account not found. Please add API keys to config.json") exit(1) self.client = Client(api_key, api_secret) # Initialize all calculated numbers self.accumulated_profit_btc = 0 self.accumulated_profit_eth = 0 self.total_btc_paid_fees = 0 self.total_eth_paid_fees = 0 self.total_btc_received = 0 self.total_eth_received = 0 def set_eth_price(self, price): self.current_eth_price = price # TODO automatically update ETH price def get_exchange_rate(self, coin="BTC", currency="USD"): """ Get BTC - USD exchange rate Bug for ETH-USD: https://community.coinbase.com/t/python-3-5-get-spot-price-for-eth-eur-returns-btc-usd/14273/9 Modify source library code: https://stackoverflow.com/a/23075617/3751589 """ param = "{}-{}".format(coin, currency) return self.client.get_spot_price(currency_pair=param) def calculate_profit_loss(self): self.current_btc_price = float( self.get_exchange_rate("BTC", "USD").amount) # self.current_eth_price = float(self.get_exchange_rate("ETH", "USD").amount) # Ask user for balance outside of Coinbase BTC_external_balance = float(input('BTC external balance: ')) ETH_external_balance = float(input('ETH external balance: ')) # # Print transactions? # print_transactions = input("Print transactions? ") # if print_transactions == "Y" or "y" or "yes" or "Yes" or "si" or "si patron": # # do everything inside for then print the following # print("\tBuy transaction: -{}".format(amount_paid_fees)) # print("\t{} transaction: {}".format(transaction.type.title(), amount_received)) # elif print_transactions == "N" or "n" or "no" or "No" or "fk off boi": # # do everything inside for loop # else: # print("Answer the question dumbass. Yes or No") # Get all accounts listing accounts = self._get_accounts() print("Accounts retrieved: {}\n".format(len(accounts.data))) # Read each account for account in accounts.data: currency = account.balance.currency if currency in ( "USD", "LTC" ) or account.name == "My Vault": # Ignore these accounts # TODO add USD wallet continue print(currency) print("Calculating currency: {}".format(currency)) print("{}: {} {}".format(account.name, account.balance.amount, currency)) # Get all transactions transactions = account.get_transactions( start_after="1805ae5b-f65b-5825-b780-9c6cecdec1cf", limit=100) """ Documentation for argument syntax in get_transactions https://github.com/coinbase/coinbase-python/blob/f9ed2249865c2012e3b86106dad5f8c6068366ed/coinbase/wallet/model.py#L168 """ # TODO regex or some way to find everyones start_after # https://stackoverflow.com/questions/44351034/pagination-on-coinbase-python-api for transaction in transactions.data: if transaction.status != "completed": print("\tIncomplete transaction...") continue # Calculate for each transaction type # Calculate all BUYS if transaction.type == "buy": transaction_id = transaction.buy.id transaction_detail = self._get_buy_transaction( account.id, transaction_id) # Calculate price point during purchase amount_paid = float(transaction_detail.subtotal.amount ) # USD paid before fees coins_bought = float( transaction_detail.amount.amount ) # Total coins received from purchase purchase_price = amount_paid / coins_bought # Price of BTC/ETH at time of buying amount_paid_fees = float( transaction.native_amount.amount ) # USD paid after fees (total paid) # Calculate profit-loss if currency == "BTC": self.accumulated_profit_btc -= amount_paid_fees self.total_btc_paid_fees += amount_paid_fees #TODO prompt user if they want to print all transactions #print("\tBuy transaction: -{}".format(amount_paid_fees)) elif currency == "ETH": self.accumulated_profit_eth -= amount_paid_fees self.total_eth_paid_fees += amount_paid_fees #TODO prompt user if they want to print all transactions #print("\tBuy transaction: -{}".format(amount_paid_fees) # Calculate all SELLS elif transaction.type in ("sell"): # Amount received after fees amount_received = float(transaction.native_amount.amount) amount_received = amount_received * -1 # Accumulate profit-loss if currency == "BTC": self.accumulated_profit_btc += amount_received self.total_btc_received += amount_received elif currency == "ETH": self.accumulated_profit_eth += amount_received self.total_eth_received += amount_received #TODO prompt user if they want to print all transactions #print("\t{} transaction: {}".format(transaction.type.title(), amount_received)) # Add current Coinbase balance + current external balance to profit/Loss if currency == "BTC": # BTC_external_value = BTC_external_balance * self.btc_current_price account.balance.amount = float(account.balance.amount) self.accumulated_profit_btc += ( BTC_external_balance + account.balance.amount) * self.current_btc_price self.percent_profit_btc = self.accumulated_profit_btc / self.total_btc_paid_fees self.total_btc_balance = ( BTC_external_balance + account.balance.amount) * self.current_btc_price elif currency == "ETH": # ETH_external_value = ETH_external_balance * self.eth_current_price account.balance.amount = float(account.balance.amount) self.accumulated_profit_eth += ( ETH_external_balance + account.balance.amount) * self.current_eth_price self.percent_profit_eth = self.accumulated_profit_eth / self.total_eth_paid_fees self.total_eth_balance = ( ETH_external_balance + account.balance.amount) * self.current_eth_price # Print accumulated profit/loss if currency == "BTC": print("\nProfit/Loss ({}): ${:.2f} or {:.2f}%".format( currency, self.accumulated_profit_btc, (self.percent_profit_btc * 100))) elif currency == "ETH": print("\nProfit/Loss ({}): ${:.2f} or {:.2f}%\n".format( currency, self.accumulated_profit_eth, (self.percent_profit_eth * 100))) # Print balance start --> balance now self.total_accumulated_profit = self.accumulated_profit_btc + self.accumulated_profit_eth self.total_paid_fees = self.total_btc_paid_fees + self.total_eth_paid_fees self.total_acc_balance = self.total_btc_balance + self.total_eth_balance + self.total_eth_received + self.total_btc_received print("\nTotal USD Value (ALL): ${:.2f} --> ${:.2f}".format( self.total_paid_fees, self.total_acc_balance)) # Print total account (BTC + ETH) profit/loss self.percent_profit_total = ( self.accumulated_profit_btc + self.accumulated_profit_eth) / ( self.total_eth_paid_fees + self.total_btc_paid_fees) * 100 print("Profit/Loss (ALL): ${:.2f} or {:.2f}%\n".format( self.accumulated_profit_btc + self.accumulated_profit_eth, self.percent_profit_total)) def _get_accounts(self): return self.client.get_accounts() def _get_buy_transaction(self, account_id, transaction_id): return self.client.get_buy(account_id, transaction_id)
def test_get_buy(self): client = Client(api_key, api_secret) buy = client.get_buy('foo', 'bar') self.assertIsInstance(buy, Buy) self.assertEqual(buy, mock_item)
class CoinbaseInterface(PlatformInterface): """ Class implementing all the methods allowing to calculate the differents overall values of accounts at given times. It allows also to enumerate all the transactions that can be impacted by taxes """ def __init__(self, api_key: str, api_secret: str, price_finder: List[PriceFinder]) -> None: # Call the upper class initialization super().__init__(price_finder) # Create the Coinbase authenticated client that we will use self.api_client = Client(api_key, api_secret) # Load all accounts and transactions fcrypt_log.info("[COINBASE][INITIALIZATION] Loading all accounts...") self._load_all_accounts() fcrypt_log.info("[COINBASE][INITIALIZATION] Loading all transactions...") self._load_all_transactions() @staticmethod def _extract_account_id(path: str) -> str: """ Function allowing to extract an account UUID from a "resource_path" given for each transaction done on Coinbase. :param path: "resource_path" to extract the account id from :type path: str :returns: str -- The account id """ items = path.split("/") # Check that the path look like we want if items[2] != "accounts": raise ValueError("It looks like the resource path is not like we want...") # Return the account id return items[3] def _load_all_accounts(self): """ This function allows to load every account that the user has on Coinbase These accounts will be used to calculate the taxes, 'in fine'. Only the accounts with an real UUID are taken into account """ accounts_list = [] other_pages = True last_id = "" while other_pages: # Get account according to pagination if last_id == "": api_answer = self.api_client.get_accounts() else: api_answer = self.api_client.get_accounts(starting_after=last_id) accounts_list = accounts_list + api_answer['data'] if ((api_answer.pagination is not None) and (api_answer.pagination["next_uri"] is not None) and (api_answer.pagination["next_uri"] != "")): # Get the pagination object pagination_obj = api_answer.pagination # Extract 'starting_after' key from next_uri parsed = urlparse.urlparse(pagination_obj["next_uri"]) last_id = parse_qs(parsed.query)['starting_after'][0] else: other_pages = False # Save all used accounts for account in accounts_list: # If the ID is a valid UUID, save the account and print it in DEBUG logs match = re.search(r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}", account['id']) if match: # Get the various values id = account['id'] name = account['name'] crypto_balance = account['balance']['amount'] crypto_currency = account['currency'] native_balance = account['native_balance']['amount'] native_currency = account['native_balance']['currency'] # Debug print fcrypt_log.debug( f"Adding account: {id} ==> {name}: {crypto_balance} {crypto_currency}" + f" ({native_balance} {native_currency})") # Add the account in our list self.accounts.append(account) def _load_all_transactions(self): """ This function allows to load every transaction that the user has done on Coinbase These transactions, in fine, will allow us to go back in time in the account, to know what was on each account, at a given time """ # Get all transactions for account in self.accounts: transactions_list = [] other_pages = True last_id = "" while other_pages: # Get account according to pagination if last_id == "": # Get the transactions for this account tmp_transactions = self.api_client.get_transactions(account['id']) else: # Get the transactions for this account tmp_transactions = self.api_client.get_transactions(account['id'], starting_after=last_id) transactions_list = transactions_list + tmp_transactions['data'] if ((tmp_transactions.pagination is not None) and (tmp_transactions.pagination["next_uri"] is not None) and (tmp_transactions.pagination["next_uri"] != "")): # Get the pagination object pagination_obj = tmp_transactions.pagination # Extract 'starting_after' key from next_uri parsed = urlparse.urlparse(pagination_obj["next_uri"]) last_id = parse_qs(parsed.query)['starting_after'][0] else: other_pages = False # Print the transactions for transaction in transactions_list: # print(transaction) transaction_type = transaction['type'] amount = transaction['amount']['amount'] currency = transaction['amount']['currency'] date = transaction['updated_at'] if not str.startswith(amount, "-"): amount = "+" + amount account = self._extract_account_id(transaction['resource_path']) fcrypt_log.debug(f"[TRANSACTION][{transaction_type}] {date}: {amount} {currency} ==> {account}") self.transactions.extend(transactions_list) def get_wallet_balance_at(self, currency: str, time: datetime.datetime) -> Decimal: """ This function allows to get the balance of a wallet at a given time. :param currency: Currency we want the value for :type currency: str :param time: Time where the value is wanted :type time: datetime.datetime :returns: Decimal -- The wallet balance at the given time """ # Firstly, get the corresponding account ID account_id = "" for account in self.accounts: if ('currency' in account) and (account['currency'] == currency): account_id = account['id'] current_balance = Decimal(account['balance']['amount']) if account_id == "": raise ValueError("No account found for this currency") # Then apply every transaction in reverse if the datetime of this transaction # is posterior to the wanted datetime for transaction in self.transactions: # Extract account ID tmp_account_id = self._extract_account_id(transaction['resource_path']) # Check if the account ids correspond if (tmp_account_id == account_id) and transaction['status'] == 'completed': # Extract the datetime operation_datetime = isoparse(transaction['updated_at']) # If datetime posterior or equal to the time given by user, reverse it if operation_datetime >= time: trans_amount = Decimal(transaction['amount']['amount']) tmp_balance = current_balance - trans_amount fcrypt_log.debug(f"[REVERSED TRANSACTION] {trans_amount} {currency} ==> {account_id}") fcrypt_log.debug( f"[REVERSED TRANSACTION] Operation {current_balance}-{trans_amount} = {tmp_balance}") current_balance = tmp_balance return current_balance def get_wallet_value_at(self, crypto_currency: str, fiat_currency: str, time: datetime.datetime) -> Decimal: """ This function allows to get the value of a wallet at a given time. :param crypto_currency: Crypto currency of the wallet :type crypto_currency: str :param fiat_currency: Fiat currency for the result (EUR, USD, etc.) :type fiat_currency: str :param time: Time where the value is wanted :type time: datetime.datetime :returns: Decimal -- The wallet value at the given time """ # Firstly, get the wallet balance at the given time balance = self.get_wallet_balance_at(crypto_currency, time) time_str = str(time) normal_balance = str(balance.normalize()) # Print debug fcrypt_log.debug(f"[WALLET] Balance at {time_str}: {normal_balance} {crypto_currency}") if balance != 0: # Now get the equivalent value in fiat rate_currency = crypto_currency + "-" + fiat_currency rate_value = self._find_rate_value_from_finders(rate_currency, time) if rate_value == Decimal(0): # Print error fcrypt_log.warning( f"[WALLET] NO RATE FOUND FOR NOT NULL BALANCE !!! Currency: {crypto_currency} \ - Fiat: {fiat_currency}") # Return 0 wallet_value = Decimal(0) else: # Calculate the wallet value wallet_value = rate_value * balance # Print info fcrypt_log.debug( f"[WALLET] Value of {crypto_currency} wallet at {time_str}: {wallet_value} {fiat_currency}") else: wallet_value = Decimal(0) return wallet_value def get_all_wallets_value(self, currency: str, time: datetime.datetime) -> Decimal: """ This function allows to get the value of all the crypto-wallets of a user at a given time :param currency: Fiat currency we want for the value (ISO 4217) :type currency: str :param time: Time where the value is wanted :type time: datetime.datetime :returns: Decimal -- The overall value at the given time """ # Initialize overall value of the wallet overall_value = Decimal(0) # For each crypto account (except fiat currency), calculate the wallet value for account in self.accounts: if account['currency'] != currency: wallet_value = self.get_wallet_value_at(account['currency'], currency, time) # Add the wallet value to the overall value overall_value += wallet_value return overall_value def all_sell_transactions_generator(self, currency: str, end_time: datetime.datetime) -> Generator: """ This function returns a generator that can be used in a for loop to get every "sell" transactions done between "start_time" and "end_time" :param currency: Fiat currency we want for the value (ISO 4217) :type currency: str :param start_time: Begin of the tax period :type start_time: datetime.datetime :param end_time: End of the tax period :type end_time: datetime.datetime :returns: Generator -- Generator to get each transaction object \ """ # Get the correct ID for this currency in order to ignore it account_to_ignore_id = self._find_account_for_currency(currency) if account_to_ignore_id == "": raise ValueError(f"Account not found with the given currency: {currency}") # Now that we have the right account for transaction in self.transactions: # Extract the account ID from the resource path account = self._extract_account_id(transaction['resource_path']) # Check that this is the right account and that is a match if (account != account_to_ignore_id) and (transaction["type"] == "sell"): # Get the amount of the transaction amount = transaction["native_amount"]["amount"] local_currency = transaction["native_amount"]["currency"] if local_currency != currency: error_msg = f"The local currency found \"{local_currency}\" does not match \ the specified currency \"{currency}\"!" raise ValueError(error_msg) # Check the time when this sell appeared transaction_time = isoparse(transaction['created_at']) if (transaction_time < end_time): # This is something we want, find the corresponding fee # Request the full sell transaction object current_sell = self.api_client.get_sell(account, transaction["sell"]["id"]) # Get the fee amount from the full sell object fee_amount = current_sell["fees"][0]["amount"]["amount"] # Declare the dictionnary to return tmp_dict = { "date": transaction_time, "currency": local_currency, "amount": amount, "fee": fee_amount } yield tmp_dict def all_buy_transactions_generator(self, currency: str, end_time: datetime.datetime) -> Generator: """ This function returns a generator that can be used in a for loop to get every "buy" transactions done before "end_time" :param currency: Fiat currency we want for the value (ISO 4217) :type currency: str :param end_time: End of the tax period :type end_time: datetime.datetime :returns: Generator -- Generator to get each transaction object """ # Get the correct ID for this currency in order to ignore it account_to_ignore_id = self._find_account_for_currency(currency) if account_to_ignore_id == "": raise ValueError(f"Account not found with the given currency: {currency}") # Now that we have the right account for transaction in self.transactions: # Extract the account ID from the resource path account = self._extract_account_id(transaction['resource_path']) # Check that this is the right account and that is a match if (account != account_to_ignore_id) and (transaction["type"] == "buy"): # Get the amount of the transaction amount = transaction["native_amount"]["amount"] local_currency = transaction["native_amount"]["currency"] if local_currency != currency: error_msg = f"The local currency found \"{local_currency}\" does not match \ the specified currency \"{currency}\"!" raise ValueError(error_msg) # Check the time when this sell appeared transaction_time = isoparse(transaction['created_at']) if (transaction_time < end_time): # This is something we want, find the corresponding fee # Request the full sell transaction object current_buy = self.api_client.get_buy(account, transaction["buy"]["id"]) # Get the fee amount from the full sell object fee_amount = current_buy["fees"][0]["amount"]["amount"] # Declare the dictionnary to return tmp_dict = { "date": transaction_time, "currency": local_currency, "amount": amount, "fee": fee_amount } yield tmp_dict
class Coinbase: def __init__(self, config, user, verbose): # TODO: Add JSON schema validation to main.py # TODO: Verify Coinbase SDK: UserWarning: Please supply API version (YYYY-MM-DD) as CB-VERSION header api_key = config[user]["coinbase_api"]["key"] api_secret = config[user]["coinbase_api"]["secret"] self.client = Client(api_key, api_secret) self.verbose = verbose # Initialize all calculated numbers self.accumulated_profit_btc = 0 self.accumulated_profit_eth = 0 self.total_btc_paid_fees = 0 self.total_eth_paid_fees = 0 self.total_btc_received = 0 self.total_eth_received = 0 def set_eth_price(self, price): """ TODO automatically update ETH price, Coinbase API error """ self.current_eth_price = price def get_exchange_rate(self, coin="BTC", currency="USD"): """ Get BTC - USD exchange rate Bug for ETH-USD: https://community.coinbase.com/t/python-3-5-get-spot-price-for-eth-eur-returns-btc-usd/14273/9 Modify source library code: https://stackoverflow.com/a/23075617/3751589 """ param = "{}-{}".format(coin, currency) return self.client.get_spot_price(currency_pair=param) def calculate_profit_loss(self): self.current_btc_price = float( self.get_exchange_rate("BTC", "USD").amount) # self.current_eth_price = float(self.get_exchange_rate("ETH", "USD").amount) # Ask user for balance outside of Coinbase BTC_external_balance = float(input('BTC external balance: ')) ETH_external_balance = float(input('ETH external balance: ')) # Get all accounts listing accounts = self._get_accounts() print("Accounts retrieved: {}\n".format(len(accounts.data))) # Read each account for account in accounts.data: currency = account.balance.currency # TODO add support for USD wallet if currency in ("USD", "LTC") or account.name == "My Vault": continue print("Calculating currency: {}".format(currency)) print("{}: {} {}".format(account.name, account.balance.amount, currency)) # Get all transactions transactions = account.get_transactions(limit=100) """ TODO regex or some way to find pagination start_after https://stackoverflow.com/questions/44351034/pagination-on-coinbase-python-api Coinbase SDK code for get_transactions https://github.com/coinbase/coinbase-python/blob/f9ed2249865c2012e3b86106dad5f8c6068366ed/coinbase/wallet/model.py#L168 """ for transaction in transactions.data: if transaction.status != "completed": print("\tIncomplete transaction...") continue # Calculate for each transaction type # Calculate all BUYS if transaction.type == "buy": transaction_id = transaction.buy.id transaction_detail = self._get_buy_transaction( account.id, transaction_id) # Calculate price point during purchase amount_paid = float(transaction_detail.subtotal.amount ) # USD paid before fees coins_bought = float( transaction_detail.amount.amount ) # Total coins received from purchase purchase_price = amount_paid / coins_bought # Price of BTC/ETH at time of buying amount_paid_fees = float( transaction.native_amount.amount ) # USD paid after fees (total paid) # TODO: Do this next -- turn into dictionary # self.total_paid_fees = {} # self.total_paid_fees[currency] # Calculate profit-loss if currency == "BTC": self.accumulated_profit_btc -= amount_paid_fees self.total_btc_paid_fees += amount_paid_fees elif currency == "ETH": self.accumulated_profit_eth -= amount_paid_fees self.total_eth_paid_fees += amount_paid_fees if (self.verbose): print( "\tBuy transaction: -{}".format(amount_paid_fees)) # Calculate all SELLS elif transaction.type in ("sell"): # Amount received after fees amount_received = float(transaction.native_amount.amount) amount_received = amount_received * -1 # Accumulate profit-loss if currency == "BTC": self.accumulated_profit_btc += amount_received self.total_btc_received += amount_received elif currency == "ETH": self.accumulated_profit_eth += amount_received self.total_eth_received += amount_received if (self.verbose): print("\t{} transaction: {}".format( transaction.type.title(), amount_received)) # Calculate total profit/loss # Add current Coinbase balance + current external balance to profit/loss if currency == "BTC": account.balance.amount = float(account.balance.amount) self.accumulated_profit_btc += ( BTC_external_balance + account.balance.amount) * self.current_btc_price self.percent_profit_btc = self.accumulated_profit_btc / self.total_btc_paid_fees self.total_btc_balance = ( BTC_external_balance + account.balance.amount) * self.current_btc_price elif currency == "ETH": account.balance.amount = float(account.balance.amount) self.accumulated_profit_eth += ( ETH_external_balance + account.balance.amount) * self.current_eth_price self.percent_profit_eth = self.accumulated_profit_eth / self.total_eth_paid_fees self.total_eth_balance = ( ETH_external_balance + account.balance.amount) * self.current_eth_price if currency == "BTC": print("\nProfit/Loss ({}): ${:.2f} or {:.2f}%".format( currency, self.accumulated_profit_btc, (self.percent_profit_btc * 100))) elif currency == "ETH": print("\nProfit/Loss ({}): ${:.2f} or {:.2f}%\n".format( currency, self.accumulated_profit_eth, (self.percent_profit_eth * 100))) # TODO: Fix this comment # TODO: `total_paid_fees` var name # TODO: Verify `total_paid_fees` value # Calculate how much you paid --> crypto assets in USD + profits you took out self.total_accumulated_profit = self.accumulated_profit_btc + self.accumulated_profit_eth self.total_paid_fees = self.total_btc_paid_fees + self.total_eth_paid_fees self.total_acc_balance = self.total_btc_balance + self.total_eth_balance + self.total_eth_received + self.total_btc_received print("\nTotal USD Value (ALL): ${:.2f} --> ${:.2f}".format( self.total_paid_fees, self.total_acc_balance)) # Print total account (BTC + ETH) profit/loss self.percent_profit_total = ( self.accumulated_profit_btc + self.accumulated_profit_eth) / ( self.total_eth_paid_fees + self.total_btc_paid_fees) * 100 print("Profit/Loss (ALL): ${:.2f} or {:.2f}%\n".format( self.accumulated_profit_btc + self.accumulated_profit_eth, self.percent_profit_total)) def _get_accounts(self): return self.client.get_accounts() def _get_buy_transaction(self, account_id, transaction_id): return self.client.get_buy(account_id, transaction_id)