commit 50fdb40a62c70f7cef477a046b492168b370dce1 Author: Nicolás Sánchez Date: Wed Jan 1 15:53:08 2025 -0300 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f9b48b3 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +credentials.py diff --git a/__pycache__/balance_accounts.cpython-311.pyc b/__pycache__/balance_accounts.cpython-311.pyc new file mode 100644 index 0000000..0ee0867 Binary files /dev/null and b/__pycache__/balance_accounts.cpython-311.pyc differ diff --git a/__pycache__/credentials.cpython-311.pyc b/__pycache__/credentials.cpython-311.pyc new file mode 100644 index 0000000..4acc2f7 Binary files /dev/null and b/__pycache__/credentials.cpython-311.pyc differ diff --git a/__pycache__/earn_binance.cpython-311.pyc b/__pycache__/earn_binance.cpython-311.pyc new file mode 100644 index 0000000..cc4ae71 Binary files /dev/null and b/__pycache__/earn_binance.cpython-311.pyc differ diff --git a/__pycache__/earn_gateio.cpython-311.pyc b/__pycache__/earn_gateio.cpython-311.pyc new file mode 100644 index 0000000..6f2d743 Binary files /dev/null and b/__pycache__/earn_gateio.cpython-311.pyc differ diff --git a/__pycache__/earn_kucoin.cpython-311.pyc b/__pycache__/earn_kucoin.cpython-311.pyc new file mode 100644 index 0000000..e87ee8c Binary files /dev/null and b/__pycache__/earn_kucoin.cpython-311.pyc differ diff --git a/__pycache__/earn_okx.cpython-311.pyc b/__pycache__/earn_okx.cpython-311.pyc new file mode 100644 index 0000000..1c61d95 Binary files /dev/null and b/__pycache__/earn_okx.cpython-311.pyc differ diff --git a/libraries/__pycache__/balance_accounts.cpython-311.pyc b/libraries/__pycache__/balance_accounts.cpython-311.pyc new file mode 100644 index 0000000..a980445 Binary files /dev/null and b/libraries/__pycache__/balance_accounts.cpython-311.pyc differ diff --git a/libraries/balance_accounts.py b/libraries/balance_accounts.py new file mode 100644 index 0000000..92bf94e --- /dev/null +++ b/libraries/balance_accounts.py @@ -0,0 +1,22 @@ +def balance_accounts(spot_balance, earn_balance, lower_limit, step_size, percentage): + target_spot_balance = (spot_balance + earn_balance) * percentage + + while abs(spot_balance - target_spot_balance) >= step_size: + if spot_balance < target_spot_balance: + spot_balance += step_size + earn_balance -= step_size + elif spot_balance > target_spot_balance: + spot_balance -= step_size + earn_balance += step_size + + # Ensure spot_balance is above lower_limit + while spot_balance < lower_limit and earn_balance >= step_size: + spot_balance += step_size + earn_balance -= step_size + + # If earn_balance is not enough, transfer the remaining amount + if spot_balance < lower_limit: + spot_balance += earn_balance + earn_balance = 0 + + return [spot_balance, earn_balance] \ No newline at end of file diff --git a/libraries/wrappers/__pycache__/earn_binance.cpython-311.pyc b/libraries/wrappers/__pycache__/earn_binance.cpython-311.pyc new file mode 100644 index 0000000..958bd0b Binary files /dev/null and b/libraries/wrappers/__pycache__/earn_binance.cpython-311.pyc differ diff --git a/libraries/wrappers/__pycache__/earn_gateio.cpython-311.pyc b/libraries/wrappers/__pycache__/earn_gateio.cpython-311.pyc new file mode 100644 index 0000000..4f324ee Binary files /dev/null and b/libraries/wrappers/__pycache__/earn_gateio.cpython-311.pyc differ diff --git a/libraries/wrappers/__pycache__/earn_kucoin.cpython-311.pyc b/libraries/wrappers/__pycache__/earn_kucoin.cpython-311.pyc new file mode 100644 index 0000000..ab955c0 Binary files /dev/null and b/libraries/wrappers/__pycache__/earn_kucoin.cpython-311.pyc differ diff --git a/libraries/wrappers/__pycache__/earn_okx.cpython-311.pyc b/libraries/wrappers/__pycache__/earn_okx.cpython-311.pyc new file mode 100644 index 0000000..5211b87 Binary files /dev/null and b/libraries/wrappers/__pycache__/earn_okx.cpython-311.pyc differ diff --git a/libraries/wrappers/earn_binance.py b/libraries/wrappers/earn_binance.py new file mode 100644 index 0000000..b5856dd --- /dev/null +++ b/libraries/wrappers/earn_binance.py @@ -0,0 +1,169 @@ +''' +https://github.com/binance/binance-connector-python/blob/master/binance/spot/_simple_earn.py + +''' +from credentials import get_api_key +from binance.spot import Spot as Client +from binance.error import ClientError + +class binance_earn: + def __init__(self): + self.api_key, self.api_secret = get_api_key("binance") + self.client = Client(self.api_key, self.api_secret) + + + def get_trading_balance(self, coin): + ''' + Returns the free available balance of a coin in the trading account (or the equivalent account in the exchange) + ''' + account = self.client.account() + for item in account["balances"]: + if item["asset"]==coin: + return item["free"] + + + def get_available_products(self, coin): + try: + response = self.client.get_simple_earn_flexible_product_list(asset=coin, current=1, size=100, recvWindow=5000) + return response + except ClientError as error: + print("Found error. status: {}, error code: {}, error message: {}".format(error.status_code, error.error_code, error.error_message)) + return None + + + def subscribe_product(self, product_id, amount, auto_subscribe=True, source_account="SPOT"): + ''' + autoSubscribe (boolean, optional): true or false, default true. + sourceAccount (str, optional): SPOT,FUND,ALL, default SPOT + recvWindow (int, optional): The value cannot be greater than 60000 + ''' + try: + response = self.client.subscribe_flexible_product(productId=product_id, amount=amount, autoSubscribe=auto_subscribe, sourceAccount=source_account, recvWindow=5000) + return response + except ClientError as error: + print("Found error. status: {}, error code: {}, error message: {}".format(error.status_code, error.error_code, error.error_message)) + return None + + + def redeem_product(self, product_id, redeem_all=True, amount=0, destination_account="SPOT"): + ''' + redeemAll (boolean, optional): true or false, default to false + amount (float, optional): if redeemAll is false, amount is mandatory + destAccount (str, optional): SPOT,FUND,ALL, default SPOT + recvWindow (int, optional): The value cannot be greater than 60000 + ''' + try: + response = self.client.redeem_flexible_product(productId=product_id, redeemAll=redeem_all, amount=amount, destAccount=destination_account, recvWindow=5000) + return response + except ClientError as error: + print("Found error. status: {}, error code: {}, error message: {}".format(error.status_code, error.error_code, error.error_message)) + return None + + + def get_position(self, **kwargs): + ''' + asset (str, optional) + productId (str, optional) + current (int, optional): Current querying page. Start from 1. Default:1 + size (int, optional): Default:10 Max:100 + recvWindow (int, optional): The value cannot be greater than 60000 + ''' + + try: + response = self.client.get_flexible_product_position(**kwargs) + return response + except ClientError as error: + print("Found error. status: {}, error code: {}, error message: {}".format(error.status_code, error.error_code, error.error_message)) + return None + + + def get_account(self, recv_window=5000): + ''' + recvWindow (int, optional): The value cannot be greater than 60000 + ''' + try: + response = self.client.simple_account(recv_window=recv_window) + return response + except ClientError as error: + print("Found error. status: {}, error code: {}, error message: {}".format(error.status_code, error.error_code, error.error_message)) + + + def get_personal_left_quota(self, product_id, **kwargs): + ''' + + ''' + try: + response = self.client.get_flexible_personal_left_quota(productId=product_id, **kwargs) + return response + except ClientError as error: + print("Found error. status: {}, error code: {}, error message: {}".format(error.status_code, error.error_code, error.error_message)) + + + def get_subscription_record(self, **kwargs): + ''' + productId (str, optional) + purchaseId (str, optional) + asset (str, optional) + startTime (int, optional): UTC timestamp in ms + endTime (int, optional): UTC timestamp in ms + current (int, optional): Current querying page. Start from 1. Default:1 + size (int, optional): Default:10 Max:100 + recvWindow (int, optional): The value cannot be greater than 60000 + ''' + try: + response = self.client.get_flexible_subscription_record(**kwargs) + return response + except ClientError as error: + print("Found error. status: {}, error code: {}, error message: {}".format(error.status_code, error.error_code, error.error_message)) + + + def get_redemption_record(self, **kwargs): + ''' + productId (str, optional) + redeemId (str, optional) + asset (str, optional) + startTime (int, optional): UTC timestamp in ms + endTime (int, optional): UTC timestamp in ms + current (int, optional): Current querying page. Start from 1. Default:1 + size (int, optional): Default:10 Max:100 + ''' + try: + response = self.client.get_flexible_redemption_record(**kwargs) + return response + except ClientError as error: + print("Found error. status: {}, error code: {}, error message: {}".format(error.status_code, error.error_code, error.error_message)) + + + def get_rewards_history(self, type="ALL", **kwargs): + ''' + type (str): ALL, Bonus, REALTIME, REWARDS + productId (str, optional) + asset (str, optional) + startTime (int, optional): UTC timestamp in ms + endTime (int, optional): UTC timestamp in ms + ''' + try: + response = self.client.get_flexible_rewards_history(type=type, **kwargs) + return response + except ClientError as error: + print("Found error. status: {}, error code: {}, error message: {}".format(error.status_code, error.error_code, error.error_message)) + + + def get_subscription_preview(self, product_id, amount, **kwargs): + ''' + args + product_id (str) + amount (float) + + kwargs + recvWindow (int, optional): The value cannot be greater than 60000 + ''' + try: + response = self.client.get_flexible_subscription_preview(productId=product_id, amount=amount, **kwargs) + return response + except ClientError as error: + print("Found error. status: {}, error code: {}, error message: {}".format(error.status_code, error.error_code, error.error_message)) + + + + diff --git a/libraries/wrappers/earn_gateio.py b/libraries/wrappers/earn_gateio.py new file mode 100644 index 0000000..3ee8e5a --- /dev/null +++ b/libraries/wrappers/earn_gateio.py @@ -0,0 +1,69 @@ +''' +https://www.gate.io/docs/developers/apiv4/#earnuni +''' + +from gate_api import ApiClient, Configuration, EarnApi, EarnUniApi +from credentials import get_api_key + +class gateio_earn: + def __init__(self): + api_key, api_secret = get_api_key("gateio") + config = Configuration(key=api_key, secret=api_secret) + self.earn_api = EarnUniApi(ApiClient(config)) + + + def get_trading_balance(self, coin): + ''' + Returns the free available balance of a coin in the trading account (or the equivalent account in the exchange) + ''' + return 0.0 + + def get_available_products(self, coin): + currency_list = self.earn_api.list_uni_currencies() + return_list = [] + for item in currency_list: + if item.to_dict()["currency"] == coin: + return_list.append(item.to_dict()) + return return_list + + + def subscribe_product(self, product_id, amount, auto_subscribe=True, source_account="SPOT"): + return None + + + def redeem_product(self, product_id, redeem_all=True, amount=0, destination_account="SPOT"): + return None + + + def get_position(self, **kwargs): + return None + + + def get_account(self, coin, recv_window=5000): + return_list = [] + earn_list = self.earn_api.list_user_uni_lends() + for item in earn_list: + if item.to_dict["currency"]==coin: + return_list.append(item.to_dict()) + return return_list + + + def get_personal_left_quota(self, product_id, **kwargs): + return None + + + def get_subscription_record(self, **kwargs): + return None + + + def get_redemption_record(self, **kwargs): + return None + + + def get_rewards_history(self, type="ALL", **kwargs): + return None + + + def get_subscription_preview(self, product_id, amount, **kwargs): + return None + \ No newline at end of file diff --git a/libraries/wrappers/earn_kucoin.py b/libraries/wrappers/earn_kucoin.py new file mode 100644 index 0000000..95281fd --- /dev/null +++ b/libraries/wrappers/earn_kucoin.py @@ -0,0 +1,127 @@ +''' +https://github.com/Kucoin/kucoin-universal-sdk/blob/main/sdk/python/README.md +''' + +from credentials import get_api_key + +from kucoin_universal_sdk.api.client import DefaultClient +from kucoin_universal_sdk.model.client_option import ClientOptionBuilder +from kucoin_universal_sdk.model.constants import GLOBAL_API_ENDPOINT, GLOBAL_FUTURES_API_ENDPOINT, GLOBAL_BROKER_API_ENDPOINT +from kucoin_universal_sdk.model.transport_option import TransportOptionBuilder +from kucoin_universal_sdk.generate.earn.earn.model_get_savings_products_req import GetSavingsProductsReqBuilder +from kucoin_universal_sdk.generate.earn.earn.model_get_account_holding_req import GetAccountHoldingReqBuilder +from kucoin_universal_sdk.generate.earn.earn.model_purchase_req import PurchaseReqBuilder +from kucoin_universal_sdk.generate.earn.earn.model_redeem_req import RedeemReqBuilder +from kucoin_universal_sdk.generate.account.account.model_get_spot_account_list_req import GetSpotAccountListReqBuilder + +class kucoin_earn: + def __init__(self): + api_key, api_secret, api_passphrase = get_api_key("kucoin") + + http_transport_option = ( + TransportOptionBuilder() + .set_keep_alive(True) + .set_max_pool_size(10) + .set_max_connection_per_pool(10) + .build() + ) + + client_option = ( + ClientOptionBuilder() + .set_key(api_key) + .set_secret(api_secret) + .set_passphrase(api_passphrase) + .set_spot_endpoint(GLOBAL_API_ENDPOINT) + .set_futures_endpoint(GLOBAL_FUTURES_API_ENDPOINT) + .set_broker_endpoint(GLOBAL_BROKER_API_ENDPOINT) + .set_transport_option(http_transport_option) + .build() + ) + + self.client = DefaultClient(client_option) + kucoin_rest_service = self.client.rest_service() + self.account_api = kucoin_rest_service.get_account_service().get_account_api + self.earn_api = kucoin_rest_service.get_earn_service().get_earn_api + + + def get_trading_balance(self, coin): + ''' + Returns the free available balance of a coin in the trading account (or the equivalent account in the exchange) + ''' + request = GetSpotAccountListReqBuilder().set_currency(coin).set_type("trade").build() + response = self.account_api().get_spot_account_list(request) + balance = response.to_dict()["data"][0]["available"] + return balance + + + def get_available_products(self, coin): + request = GetSavingsProductsReqBuilder().set_currency(coin).build() + response = self.earn_api().get_savings_products(request) + return response.to_dict() + + + def subscribe_product(self, product_id, amount, auto_subscribe=True, source_account="SPOT"): + ''' + source_account: "TRADE" or "MAIN" + auto_subscribe is ignored here + ''' + if source_account.upper() in ["SPOT","TRADE","ALL"]: + source="trade" + elif source_account.upper() in ["FUND","MAIN"]: + source="main" #In Binance SPOT is TRADE and MAIN is FUND + else: + return {"Error": "Invalid source_account. Values should be TRADE or MAIN for kucoin, SPOT, FUND or ALL for binance"} + + request = PurchaseReqBuilder(product_id=product_id, amount=str(amount), account_type=source).build() + response = self.earn_api().purchase(request) + return response.to_dict() + + + def redeem_product(self, order_id, amount="0", source_account="SPOT"): + ''' + source_account is ignored unless orderID=ETH2: "TRADE" or "MAIN" + auto_subscribe is ignored here + ''' + + if source_account in ["SPOT","TRADE","ALL"]: + source="TRADE" + elif source_account in ["FUND","MAIN"]: + source="MAIN" #In Binance SPOT is TRADE and MAIN is FUND + else: + return {"Error": "Invalid source_account. Values should be TRADE or MAIN for kucoin, SPOT, FUND or ALL for binance"} + + request = RedeemReqBuilder(order_id=order_id, amount=str(amount), from_account_type=source).build() + response = self.earn_api().purchase(request) + return response.to_dict() + + + def get_position(self, coin, **kwargs): + request = GetAccountHoldingReqBuilder().set_currency(coin).build() + response = self.earn_api().get_account_holding(request) + return response.to_dict() + + + def get_account(self,coin,**kwargs): + request = GetAccountHoldingReqBuilder().build() + response = self.earn_api().get_account_holding(request) + return response.to_dict() + + def get_personal_left_quota(self, *args, **kwargs): + return {"Error": "N/A"} + + + def get_subscription_record(self, **kwargs): + return {"Error": "N/A"} + + + def get_redemption_record(self, **kwargs): + return {"Error": "N/A"} + + + def get_rewards_history(self, type="ALL", **kwargs): + return {"Error": "N/A"} + + + def get_subscription_preview(self, product_id, amount, **kwargs): + return {"Error": "N/A"} + \ No newline at end of file diff --git a/libraries/wrappers/earn_okx.py b/libraries/wrappers/earn_okx.py new file mode 100644 index 0000000..2c16d6d --- /dev/null +++ b/libraries/wrappers/earn_okx.py @@ -0,0 +1,183 @@ +''' +https://www.okx.com/docs-v5/en/#financial-product-simple-earn-flexible + +https://github.com/okxapi/python-okx/blob/master/okx/Earning.py + +''' + +import okx.Earning as Earning +import okx.Funding as Funding +import okx.Account as Account +from credentials import get_api_key + + +class okx_earn: + def __init__(self): + api_key, api_secret, passphrase = get_api_key("okx") + self.earning_api = Earning.EarningAPI(api_key, api_secret, passphrase, False, "0") + self.funding_api = Funding.FundingAPI(api_key, api_secret, passphrase, False, "0") + self.account_api = Account.AccountAPI(api_key, api_secret, passphrase, False, "0") + + + def get_trading_balance(self, coin): + ''' + Returns the free available balance of a coin in the trading account (or the equivalent account in the exchange) + ''' + balances = self.account_api.get_account_balance() + for item in balances["data"][0]["details"]: + if item["ccy"] == coin: + return item["availBal"] + return 0.0 + + + def get_funding_balance(self, coin): + balances = self.funding_api.get_balances() + for item in balances["data"]: + if item["ccy"] == coin: + return item["availBal"] + return 0.0 + + + def transfer_to_funding(self,coin,amount): + ''' + Transfer funds from the trading account to the funding account + + Response example: + { + "code": "0", + "msg": "", + "data": [ + { + "transId": "754147", + "ccy": "USDT", + "clientId": "", + "from": "6", + "amt": "0.1", + "to": "18" + } + ] + } + ''' + + transfer = self.funding_api.funds_transfer(coin,amount,"18","6","0") + return transfer + + + def transfer_to_trading(self,coin,amount): + ''' + Transfer funds from the funding account to the trading account + + Response example: + { + "code": "0", + "msg": "", + "data": [ + { + "transId": "754147", + "ccy": "USDT", + "clientId": "", + "from": "6", + "amt": "0.1", + "to": "18" + } + ] + } + ''' + transfer = self.funding_api.funds_transfer(coin,amount,"6","18","0") + return transfer + + + def get_transfer_state(self, transaction_id): + ''' + Gets the state of a transfer. + Sometimes the transfer request fails, even if the transfer was successful. + + Sample response: + { + "code": "0", + "data": [ + { + "amt": "1.5", + "ccy": "USDT", + "clientId": "", + "from": "18", + "instId": "", //deprecated + "state": "success", + "subAcct": "test", + "to": "6", + "toInstId": "", //deprecated + "transId": "1", + "type": "1" + } + ], + "msg": "" + } + ''' + transfer = self.funding_api.transfer_state(transaction_id) + return transfer + + + def get_available_products(self, coin): + return self.earning_api.get_public_borrow_info(coin) + + + def subscribe_product(self, coin, amount, rate=None): + ''' + ONLY ASSETS IN THE FUNDING ACCOUNT CAN BE USED FOR SUBSCRIPTION, MOVE THE FUNDS FROM + THE TRADING ACCOUNT TO THE FUNDING ACCOUNT BEFORE SUBSCRIBING. + ''' + if rate is None: + rate = self.get_avg_rate(coin) + return self.earning_api.savings_purchase_redemption(coin, str(amount), "purchase", str(rate)) + + + def redeem_product(self, coin, amount): + ''' + ASSETS REDEEMED WILL BE PLACED IN THE FUNDING ACCOUNT, MOVE THE FUNDS FROM THE FUNDING ACCOUNT + TO THE TRADING ACCOUNT AFTER REDEMPTION. + ''' + return self.earning_api.savings_purchase_redemption(coin, str(amount), "redempt", "0") + + + def set_rate(self, coin, rate): + return self.earning_api.set_lending_rate(coin, str(rate)) + + + def get_avg_rate(self,coin): + ''' + Returns the 24hs average lending rate + ''' + rate = self.earning_api.get_public_borrow_info(coin) + return str(rate["data"][0]["avgRate"]) + + + def get_position(self, coin, **kwargs): + return self.earning_api.get_saving_balance(coin) + + + def get_account(self, coin, recv_window=5000): + return self.earning_api.get_saving_balance() + + + def get_lending_history(self,coin=None): + return self.earning_api.get_lending_history(coin) + + + def get_personal_left_quota(self, product_id, **kwargs): + return None + + + def get_subscription_record(self, **kwargs): + return None + + + def get_redemption_record(self, **kwargs): + return None + + + def get_rewards_history(self, type="ALL", **kwargs): + return None + + + def get_subscription_preview(self, product_id, amount, **kwargs): + return None \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..14d1613 --- /dev/null +++ b/main.py @@ -0,0 +1,17 @@ +import libraries.balance_accounts as balance_accounts +from libraries.wrappers import earn_binance +from libraries.wrappers import earn_kucoin +from libraries.wrappers import earn_okx +from libraries.wrappers import earn_gateio +import json + +kucoin = earn_kucoin.kucoin_earn() +binance = earn_binance.binance_earn() +okx = earn_okx.okx_earn() +gateio = earn_gateio.gateio_earn() + +if __name__=="__main__": + #Available products + print(okx.get_funding_balance("USDT")) + +