Compare commits

...

25 Commits

Author SHA1 Message Date
Nicolás Sánchez 595dbcdb54 fixed OKX not subscribing 2025-11-20 09:31:32 -03:00
Nicolás Sánchez e6d4578807 added okx rate debug log entry 2025-11-18 16:38:25 -03:00
Nicolás Sánchez 23d09487d8 if clause corrected 2025-10-11 10:09:19 -03:00
Nicolás Sánchez c1ad535302 Removed 0 earning balance conditional 2025-10-11 09:58:25 -03:00
Nicolás Sánchez 2a01477a5e cleanup 2025-08-19 20:35:44 -03:00
Nicolás Sánchez b383cde1fe minor api-key handling refactor 2025-08-19 20:12:26 -03:00
Nicolás Sánchez 9ed2927c0b 2025.08.19 - Improved thread handling 2025-08-19 19:15:47 -03:00
Nicolás Sánchez 86ed6830f4 force_pause now optional in subscribe/redeem 2025-06-23 17:46:11 -03:00
Nicolás Sánchez 17a5081f8a global_status now includes currency 2025-03-13 12:41:39 -03:00
Nicolás Sánchez 866a130c35 minor typos and fixes 2025-03-10 19:13:38 -03:00
Nicolás Sánchez ad1adeff0a error handling 2025-02-21 10:23:37 -03:00
Nicolás Sánchez 574d8f82ef new endpoint get_global_status 2025-01-29 09:49:14 -03:00
Nicolás Sánchez 15b2cf5601 logging formatting 2025-01-17 20:17:31 -03:00
Nicolás Sánchez 4ed36e1482 version number 2025-01-11 21:57:28 -03:00
Nicolás Sánchez e011621529 subscriptions/redemptions messages colored 2025-01-11 18:37:48 -03:00
Nicolás Sánchez d45d52f34f wider bars 2025-01-10 18:05:09 -03:00
Nicolás Sánchez 394c71035d subscribe and redeem endpoints 2025-01-10 16:03:06 -03:00
Nicolás Sánchez b08f320fe1 README 2025-01-10 09:39:36 -03:00
Nicolás Sánchez 7704ea3ea7 . 2025-01-09 21:52:42 -03:00
Nicolás Sánchez 85bc56c11d migration-test done 2025-01-09 20:20:42 -03:00
Nicolás Sánchez a62dc459e3 migration-test 2025-01-09 20:20:00 -03:00
Nicolás Sánchez f0bfcacc45 kucoin transfers 2025-01-09 14:30:29 -03:00
Nicolás Sánchez 8e999d5049 bug fixes 2025-01-09 12:12:41 -03:00
Nicolás Sánchez e7374b1586 decimals 2025-01-09 08:06:45 -03:00
Nicolás Sánchez 123a0daf7f work work work 2025-01-08 18:19:06 -03:00
13 changed files with 642 additions and 285 deletions

1
.gitignore vendored
View File

@ -6,3 +6,4 @@ keys/api_credentials.db
*.pyo
credentials.py
some_tests.py
tests.py

65
README.md Normal file
View File

@ -0,0 +1,65 @@
# DCAv2Earn
DCAv2Earn is a companion project for DCAv2. It automates the process of allocating a portion of idle funds on your exchange accounts to earn programs provided by each exchange. The system ensures that your funds are efficiently utilized to generate passive income while maintaining a minimum balance in your trading accounts at all times.
## Features
- **Automatic Allocation**: Automatically transfers a configurable percentage of your idle funds to the earn program of each supported exchange.
- **Configurable Parameters**:
- **Percentage of Funds**: Specify what percentage of your idle funds should be allocated to the earn program.
- **Minimum Balance**: Set a minimum balance that must remain in your trading account to prevent over-allocation.
- **Step Size**: Define the step size for balance adjustments to ensure smooth transitions.
- **Multiple Exchanges Support**: Currently supports Binance, Gate.io, KuCoin and OKX. More exchanges can be added as needed.
- **Logging and Monitoring**: Provides real-time monitoring of your funds' allocation and performance.
## Prerequisites
- **Python 3.8+**: Ensure you have Python 3.8 or higher installed.
- **API Keys**: Obtain API keys from the supported exchanges with the necessary permissions for transferring funds (NOT withdrawing) and accessing earn programs.
## Installation
1. **Clone the Repository**:
```bash
git clone https://gitea.nicosanchez.com.ar/nicolas/DCAv2Earn.git
cd DCAv2Earn
```
2. **Configure the System**:
- Create a `config.json` file with the necessary configuration parameters.
- Use `config.json.example` as a template.
3. **Load the credentials to credentials.py**
```
Check credentials.py.example and replace the placeholders with your actual API keys. Then, rename the file to credentials.py.
```
4. **Run the System**:
```bash
python main.py
```
## Configuration
- **lap_time**: The time in seconds between each lap of the system. During each lap, the system checks and adjusts your funds' allocation.
- **minimum_amount_in_trading_account**: The minimum amount of funds that must remain in your trading account at all times to prevent over-allocation.
- **percentage_of_funds_to_commit**: The percentage of your idle funds to allocate to the earn program.
- **step_size**: The step size for balance adjustments, ensuring smooth transitions between balances.
- **api_keys**: API keys for each supported exchange with the necessary permissions. The API keys are stored in credentials.py
## Usage
### ATTENTION: THIS PROGRAM IS A PERSONAL PROJECT AND IS NOT INTENDED TO BE USED BY ANYONE ELSE. USE AT YOUR OWN RISK.
The system runs continuously in the background, automatically allocating your funds to the earn program according to the specified parameters. You can monitor the system's performance and status through the provided logging and monitoring features.
## Support
For any questions, issues, or feature requests, please create an issue on the Gitea repository (https://gitea.nicosanchez.com.ar/nicolas/DCAv2Earn/issues).
## License
DCAv2Earn is released under the MIT License.
---

View File

@ -3,7 +3,7 @@
"binance": {
"currency": "USDT",
"minimum_amount_in_trading_account": 1000,
"step_size": 500,
"step_size": 250,
"percentage": 0.5,
"time_between_subscriptions": 3600,
"time_between_redemptions": 0
@ -11,7 +11,7 @@
"gateio": {
"currency": "USDT",
"minimum_amount_in_trading_account": 1000,
"step_size": 500,
"step_size": 250,
"percentage": 0.5,
"time_between_subscriptions": 3600,
"time_between_redemptions": 0
@ -19,7 +19,7 @@
"kucoin": {
"currency": "USDT",
"minimum_amount_in_trading_account": 1000,
"step_size": 500,
"step_size": 250,
"percentage": 0.5,
"time_between_subscriptions": 3600,
"time_between_redemptions": 0
@ -27,13 +27,13 @@
"okx": {
"currency": "USDT",
"minimum_amount_in_trading_account": 1000,
"step_size": 500,
"step_size": 250,
"percentage": 0.5,
"time_between_subscriptions": 3600,
"time_between_redemptions": 0
}
},
"lap_time": 10,
"lap_time": 30,
"api_port": 5011
}

39
config.json.example Normal file
View File

@ -0,0 +1,39 @@
{
"exchanges": {
"binance": {
"currency": "USDT",
"minimum_amount_in_trading_account": 1000,
"step_size": 250,
"percentage": 0.5,
"time_between_subscriptions": 3600,
"time_between_redemptions": 0
},
"gateio": {
"currency": "USDT",
"minimum_amount_in_trading_account": 1000,
"step_size": 250,
"percentage": 0.5,
"time_between_subscriptions": 3600,
"time_between_redemptions": 0
},
"kucoin": {
"currency": "USDT",
"minimum_amount_in_trading_account": 1000,
"step_size": 250,
"percentage": 0.5,
"time_between_subscriptions": 3600,
"time_between_redemptions": 0
},
"okx": {
"currency": "USDT",
"minimum_amount_in_trading_account": 1000,
"step_size": 250,
"percentage": 0.5,
"time_between_subscriptions": 3600,
"time_between_redemptions": 0
}
},
"lap_time": 30,
"api_port": 5001
}

21
credentials.py.example Normal file
View File

@ -0,0 +1,21 @@
def get_api_key(exchange):
if exchange=="binance":
api_key = "place_your_api_key_here"
api_secret = "place_your_api_secret_here"
return api_key, api_secret
elif exchange=="kucoin":
api_key = "place_your_api_key_here"
api_secret = "place_your_api_secret_here"
api_passphrase = "place_your_passphrase_here"
return api_key, api_secret, api_passphrase
elif exchange=="okx":
api_key = "place_your_api_key_here"
api_secret = "place_your_api_secret_here"
api_passphrase = "place_your_passphrase_here"
return api_key, api_secret, api_passphrase
elif exchange=="gateio":
api_key = "place_your_api_key_here"
api_secret = "place_your_api_secret_here"
return api_key, api_secret
else:
return None,None

View File

@ -1,9 +1,28 @@
def balance_accounts(spot_balance, earn_balance, lower_limit, step_size, percentage):
'''
args:
spot_balance: float
Current spot balance
earn_balance: float
Current earn balance
lower_limit: float
Lower limit for spot balance
step_size: float
Step size for balance adjustment
percentage: float
Percentage of funds to be allocated to the earn account
returns:
spot_balance: float
Updated spot balance
earn_balance: float
Updated earn balance
'''
spot_balance+=earn_balance
earn_balance=0
#target_spot_balance = (spot_balance + earn_balance) * percentage
target_spot_balance = spot_balance * percentage
target_spot_balance = spot_balance * (1-percentage)
while abs(spot_balance - target_spot_balance) >= step_size:
if spot_balance < target_spot_balance:

View File

@ -1,6 +1,8 @@
import time
import datetime
from libraries.balance_accounts import balance_accounts
from libraries.colors import colors
from decimal import Decimal, getcontext, ROUND_DOWN
class earner:
@ -20,14 +22,24 @@ class earner:
self.trading_balance = 0
self.earning_balance = 0
self.is_paused = True
self.is_paused = False
self.status_string = ""
getcontext().prec = 8
getcontext().rounding = ROUND_DOWN
def __str__(self):
return str(self.connector)
def get_currency(self):
return self.currency
def write_to_log(self,message):
with open("earn.log", "a") as f:
f.write(datetime.datetime.now().strftime(f"[%Y/%m/%d %H:%M:%S] {str(self.connector).upper()} | {message}\n"))
def toggle_pause(self):
self.is_paused = not self.is_paused
return self.is_paused
@ -41,6 +53,7 @@ class earner:
return self.step_size
except Exception as e:
print(e)
self.write_to_log(str(e))
return 0
def get_step_size(self):
@ -52,6 +65,7 @@ class earner:
return self.percentage
except Exception as e:
print(e)
self.write_to_log(str(e))
return 0
def get_percentage(self):
@ -63,6 +77,7 @@ class earner:
return self.minimum_amount_in_trading_account
except Exception as e:
print(e)
self.write_to_log(str(e))
return 0
def get_minimum_amount_in_trading_account(self):
@ -74,6 +89,7 @@ class earner:
return self.time_between_subscriptions
except Exception as e:
print(e)
self.write_to_log(str(e))
return 0
def get_time_between_subscriptions(self):
@ -85,6 +101,7 @@ class earner:
return self.time_between_redemptions
except Exception as e:
print(e)
self.write_to_log(str(e))
return 0
def get_time_between_redemptions(self):
@ -100,63 +117,97 @@ class earner:
return self.status_string
def get_trading_balance(self):
try:
return float(self.trading_balance)
except Exception as e:
self.write_to_log(f"{e}")
return 0.0
def get_earning_balance(self):
try:
return float(self.earning_balance)
except Exception as e:
self.write_to_log(f"{e}")
return 0.0
def subscribe(self,amount,force_pause=False):
print(f"{datetime.datetime.now().strftime('[%Y/%m/%d %H:%M:%S]')} | {str(self.connector).upper()} | {colors.green}Subscribing{colors.white} {amount} {self.currency}")
self.write_to_log(f"{colors.green}Subscribing{colors.white} {amount} {self.currency}")
def subscribe(self,amount):
print(f"{str(self.connector)} | Subscribing {amount} {self.currency}")
if force_pause:
self.pause = True
available_product = self.connector.get_available_products(self.currency)
subscription = {}
if available_product["asset"]==self.currency:
if available_product["coin"]==self.currency:
#Every exchange has it own subscription method
if str(self.connector) in ["binance","kucoin"]:
subscription = self.connector.subscribe_product(available_product["productId"],amount)
subscription = self.connector.subscribe_product(available_product["product_id"],amount)
elif str(self.connector)=="gateio":
min_rate = self.connector.get_min_rate(available_product["asset"])["min_rate"]
min_rate = self.connector.get_min_rate(available_product["coin"])["min_rate"]
time.sleep(.5) #For the sake of the API
subscription = self.connector.subscribe_product(available_product["productId"],amount,min_rate)
subscription = self.connector.subscribe_product(available_product["product_id"],amount,min_rate)
elif str(self.connector)=="okx":
transfer = self.connector.transfer_to_funding(available_product["asset"],str(amount))
transfer = self.connector.transfer_to_funding(available_product["coin"],str(amount))
if "Success" in transfer:
transfer_state = self.connector.get_transfer_state(transfer["transId"])
if "Success" in transfer_state:
subscription = self.connector.subscribe_product(available_product["productId"],amount)
subscription = self.connector.subscribe_product(available_product["product_id"],amount)
else:
print(f"{str(self.connector)} - Transfer of funds state query failed!")
self.write_to_log("Transfer of funds state query failed! - " + str(subscription))
return 1
else:
print(f"{str(self.connector)} - Transfer of funds failed!")
self.write_to_log("Transfer of funds failed! - " + str(transfer))
return 1
if "Success" in subscription:
self.last_subscription_time = time.time()
return 0
self.write_to_log("Subscription failed! - " + str(subscription))
return 1
def redeem(self,amount,force_pause=False):
print(f"{datetime.datetime.now().strftime('[%Y/%m/%d %H:%M:%S]')} | {str(self.connector).upper()} | {colors.red}Redeeming{colors.white} {amount} {self.currency}")
self.write_to_log(f"{colors.red}Redeeming{colors.white} {amount} {self.currency}")
def redeem(self,amount):
print(f"{str(self.connector)} | Redeeming {amount} {self.currency}")
if force_pause:
self.pause = True
available_product = self.connector.get_available_products(self.currency)
redemption = {}
if available_product["asset"]==self.currency:
if available_product["coin"]==self.currency:
if str(self.connector) in ["binance","gateio"]:
redemption = self.connector.redeem_product(available_product["productId"],amount=amount)
redemption = self.connector.redeem_product(available_product["product_id"],amount=amount)
elif str(self.connector)=="kucoin":
position = self.connector.get_position(self.currency)
if "Error" not in position:
redemption = self.connector.redeem_product(position["orderId"],amount=amount)
redemption = self.connector.redeem_product(position["positionId"],amount=amount)
time.sleep(1)
#The funds go to the funding account - transfer them to the trading account.
transfer_step = self.connector.transfer_to_trading(self.currency, amount)
else:
print(f"{str(self.connector)} - Position not found!")
self.write_to_log("Position not found! " + str(position))
return 1
elif str(self.connector)=="okx":
redemption_step = self.connector.redeem_product(self.currency, amount=amount)
if "Success" in redemption_step:
transfer_step = self.connector.transfer_to_trading(self.currency, redemption_step["amount"])
time.sleep(1) #Breathing room
funding_balance = self.connector.get_funding_balance(self.currency)
transfer_step = self.connector.transfer_to_trading(self.currency, funding_balance[self.currency])
if "Success" in transfer_step:
redemption = self.connector.get_transfer_state(transfer_step["transId"])
else:
print(f"{str(self.connector)} - Redemption failed!")
print(f"{str(self.connector)} - Transfer step failed!")
self.write_to_log("Transfer step failed! " + str(transfer_step))
return 1
if "Sucess" in redemption:
else:
print(f"{str(self.connector)} - Redemption step failed!")
self.write_to_log("Redemption step failed! " + str(redemption_step))
return 1
if "Success" in redemption:
self.last_redemption_time = time.time()
return 0
self.write_to_log("Redemption failed! - " + str(redemption))
return 1
@ -185,30 +236,31 @@ class earner:
else:
self.earning_balance = None
paused_string = ""
if not self.is_paused:
target_trading_amount, target_earning_amount = balance_accounts(float(self.trading_balance),
float(self.earning_balance),
self.minimum_amount_in_trading_account,
self.step_size,
self.percentage)
earning_delta = 0
target_trading_amount, target_earning_amount = balance_accounts(Decimal(self.trading_balance),
Decimal(self.earning_balance),
Decimal(self.minimum_amount_in_trading_account),
Decimal(self.step_size),
Decimal(self.percentage))
earning_delta = Decimal(0)
if self.earning_balance is None:
print(f"{str(self.connector)} - There was an error fetching earning balance")
print(f"{str(self.connector).upper()} - There was an error fetching earning balance")
else:
if float(self.earning_balance)!=0:
earning_delta = target_earning_amount - float(self.earning_balance)
if earning_delta>self.step_size and time.time()-self.last_subscription_time>self.time_between_subscriptions:
#if float(self.earning_balance)!=0:
earning_delta = target_earning_amount - Decimal(self.earning_balance)
if earning_delta>Decimal(self.step_size) and time.time()-self.last_subscription_time>self.time_between_subscriptions:
self.subscribe(earning_delta)
if earning_delta<-self.step_size and time.time()-self.last_redemption_time>self.time_between_redemptions:
elif earning_delta<-Decimal(self.step_size) and time.time()-self.last_redemption_time>self.time_between_redemptions:
self.redeem(-earning_delta)
print(f"{str(self.connector)} - Difference: {earning_delta}")
#print(f"{str(self.connector)} - Difference: {earning_delta}")
else:
paused_string = "| "+colors.yellow+"PAUSED"+colors.white
#Output status to status_string
balances_string = f"Trading balance: {float(self.trading_balance):.2f} {self.currency} | Earning balance: {float(self.earning_balance):.2f} {self.currency}"
percentages_string = f"On earn: {float(self.earning_balance)/float(self.trading_balance):.2%} {paused_string}"
total_balance = float(self.earning_balance)+float(self.trading_balance)
percentages_string = f"On earn: {float(self.earning_balance)/total_balance:.2%} {paused_string}"
self.status_string = f"{colors.cyan}{self}{colors.white} | {balances_string} | {percentages_string}"
self.status_string = f"{colors.cyan}{str(self).upper()}{colors.white} | {balances_string} | {percentages_string}"

View File

@ -83,7 +83,7 @@ class binance_earn:
example: {'redeemId': 483738233, 'success': True}
'''
try:
response = self.client.redeem_flexible_product(productId=product_id, redeemAll=redeem_all, amount=amount, destAccount=destination_account, recvWindow=5000)
response = self.client.redeem_flexible_product(product_id, redeemAll=redeem_all, amount=amount, destAccount=destination_account, recvWindow=5000)
if response["success"]:
return {"Success": "",
"orderId": response["redeemId"],

View File

@ -207,8 +207,16 @@ class gateio_earn:
return {"Error": "To be implemented"}
def get_rewards_history(self, type="ALL", **kwargs):
return {"Error": "To be implemented"}
def get_rewards_history(self, coin):
url = f"/earn/uni/interests/{coin}"
headers = {"Accept": "application/json", "Content-Type": "application/json"}
sign_headers = self.gen_sign("GET", self.prefix+url)
headers.update(sign_headers)
response = requests.get(self.host+self.prefix+url, headers=headers)
if response.status_code == 200:
return response.json()
else:
return {"Error": response.text}
def get_subscription_preview(self, product_id, amount, **kwargs):

View File

@ -13,6 +13,7 @@ from kucoin_universal_sdk.generate.earn.earn.model_get_account_holding_req impor
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
from kucoin_universal_sdk.generate.account.transfer.model_flex_transfer_req import FlexTransferReqBuilder
class kucoin_earn:
def __init__(self):
@ -42,6 +43,7 @@ class kucoin_earn:
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
self.transfer_api = kucoin_rest_service.get_account_service().get_transfer_api
def __str__(self):
@ -127,6 +129,27 @@ class kucoin_earn:
return {"Error": response}
def transfer_to_trading(self, coin, amount):
'''
Args:
coin (str): The coin to transfer to trading
amount (float): The amount to transfer
Returns:
dict: The response from the api
'''
request = FlexTransferReqBuilder().set_from_account_type("MAIN").set_to_account_type("TRADE").set_currency(coin).set_amount(str(amount)).set_type("INTERNAL").set_client_oid("1234").build()
response = self.transfer_api().flex_transfer(request).to_dict()
response_dict = response["common_response"]["data"]
if "orderId" in response_dict:
return {"Success": "",
"orderId": response["orderId"],
"txId": "",
"amount": amount
}
else:
return {"Error": response}
def get_position(self, coin):
'''
Return {'common_response': {'code': '200000', 'data': {'totalNum': 1, 'items': [{'orderId': '2987632', 'productId': '2152', 'productCategory': 'DEMAND', 'productType': 'DEMAND', 'currency': 'USDT', 'incomeCurrency': 'USDT', 'returnRate': '0.04767484', 'holdAmount': '20', 'redeemedAmount': '0', 'redeemingAmount': '0', 'lockStartTime': 1641806718000, 'lockEndTime': None, 'purchaseTime': 1736027283000, 'redeemPeriod': 0, 'status': 'LOCKED', 'earlyRedeemSupported': 0}], 'currentPage': 1, 'pageSize': 15, 'totalPage': 1}, 'rate_limit': {'limit': 2000, 'remaining': 1995, 'reset': 16550}}, 'totalNum': 1, 'items': [{'orderId': '2987632', 'productId': '2152', 'productCategory': 'DEMAND', 'productType': 'DEMAND', 'currency': 'USDT', 'incomeCurrency': 'USDT', 'returnRate': '0.04767484', 'holdAmount': '20', 'redeemedAmount': '0', 'redeemingAmount': '0', 'lockStartTime': 1641806718000, 'purchaseTime': 1736027283000, 'redeemPeriod': 0, 'status': <StatusEnum.LOCKED: 'LOCKED'>, 'earlyRedeemSupported': <EarlyRedeemSupportedEnum.T_0: 0>}], 'currentPage': 1, 'pageSize': 15, 'totalPage': 1}

View File

@ -177,7 +177,7 @@ class okx_earn:
"amount": response["data"][0]["amt"]
}
else:
return {"Error": response}
return {"Error": response, "Rate": str(rate)}
def redeem_product(self, coin, amount):
@ -207,6 +207,8 @@ class okx_earn:
Returns the 24hs average lending rate
'''
rate = self.earning_api.get_public_borrow_info(coin)
if rate["data"][0]["avgRate"]=='0':
return str(rate["data"][0]["estRate"])
return str(rate["data"][0]["avgRate"])

262
main.py
View File

@ -1,23 +1,25 @@
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
from libraries.earner import earner
from libraries.colors import colors
from concurrent.futures import ThreadPoolExecutor, as_completed
from threading import Thread
from flask import Flask, jsonify, request
from waitress import serve
from functools import wraps
import time
import datetime
import json
import sqlite3
import socket
import signal
import os
def load_keys_from_db(file_name: str) -> list:
'''
Load valid API keys
Load valid API keys to a set
Parameters
----------
@ -36,7 +38,7 @@ def load_keys_from_db(file_name: str) -> list:
data = database_cursor.fetchall()
database_connection.close()
valid_keys = [line[1] for line in data]
valid_keys = {line[1] for line in data}
return valid_keys
@ -63,16 +65,15 @@ def seconds_to_time(total_seconds: float) -> str:
def main():
executor = ThreadPoolExecutor(max_workers=len(earners))
while True:
threads = []
#Run earners
for item in earners:
threads.append(Thread(target=item.run))
for item in threads:
item.start()
for item in threads:
item.join()
futures = [executor.submit(e.run) for e in earners]
for future in as_completed(futures):
try:
future.result()
except Exception as e:
print(f"Error in thread - {e}")
#Print status
subscriptions = []
@ -88,7 +89,7 @@ def main():
last_subscription_key = "Never"
last_subscription_value = ""
else:
last_subscription_value = datetime.datetime.fromtimestamp(last_subscription_value).strftime('%Y-%m-%d %H:%M:%S')
last_subscription_value = datetime.datetime.fromtimestamp(last_subscription_value).strftime('@%Y/%m/%d %H:%M:%S')
last_redemption = max(redemptions, key=lambda d: next(iter(d.values())))
last_redemption_key = next(iter(last_redemption.keys()))
last_redemption_value = next(iter(last_redemption.values()))
@ -96,15 +97,17 @@ def main():
last_redemption_key = "Never"
last_redemption_value = ""
else:
last_redemption_value = datetime.datetime.fromtimestamp(last_redemption_value).strftime('%Y-%m-%d %H:%M:%S')
print("-"*80)
last_redemption_value = datetime.datetime.fromtimestamp(last_redemption_value).strftime('@%Y/%m/%d %H:%M:%S')
print("-"*90)
total_on_trading = sum([item.get_trading_balance() for item in earners])
total_on_earning = sum([item.get_earning_balance() for item in earners])
total_funds = total_on_earning+total_on_trading
time_of_day = datetime.datetime.now().strftime('[%Y/%m/%d %H:%M:%S]')
print(f"{time_of_day} | Version {version} | Total funds: {total_on_trading+total_on_earning:.2f} | On earn: {total_on_earning:.2f} ({total_on_earning/total_on_trading*100:.2f}%)")
print(f"Uptime: {seconds_to_time(time.time()-start_time)} | Last subscription: {last_subscription_key} - {last_subscription_value} | Last redemption: {last_redemption_key} - {last_redemption_value}")
print(colors.blue+"="*80+colors.white)
print(f"Last subscription: {last_subscription_key}{last_subscription_value} | Last redemption: {last_redemption_key}{last_redemption_value}")
print(f"Version {version} | Total funds: {total_funds:.2f} USDT | On earn: {total_on_earning:.2f} USDT ({total_on_earning/total_funds*100:.2f}%)")
print(f"{time_of_day} | Uptime: {seconds_to_time(time.time()-start_time)}")
print(colors.blue+"="*90+colors.white)
#Wait for next lap
time.sleep(config["lap_time"])
@ -116,7 +119,48 @@ def main():
earn_api = Flask(__name__)
#Helper functions
def require_api_key(func):
'''
Validates API key
'''
@wraps(func)
def wrapper(*args, **kwargs):
key = request.headers.get("X-API-KEY")
if not key or key not in valid_keys:
return jsonify({'Error': 'API key not valid'}), 401
return func(*args, **kwargs)
return wrapper
@earn_api.route("/get_global_status", methods=['GET'])
@require_api_key
def get_global_status():
'''
GET request
Parameters:
None
'''
response = {}
for item in earners:
response[str(item.connector)] = {"currency": item.get_currency(),
"trading_balance": item.get_trading_balance(),
"earning_balance": item.get_earning_balance(),
"is_paused": item.get_is_paused(),
"step_size": item.get_step_size(),
"percentage": item.get_percentage(),
"minimum_amount_in_trading_account": item.get_minimum_amount_in_trading_account(),
"time_between_subscriptions": item.get_time_between_subscriptions(),
"time_between_redemptions": item.get_time_between_redemptions(),
"last_subscription": item.get_last_subscription(),
"last_redemption": item.get_last_redemption()}
response["uptime"] = time.time() - start_time
return jsonify(response)
@earn_api.route("/toggle_pause", methods=['POST'])
@require_api_key
def toggle_pause():
'''
GET request
@ -125,24 +169,22 @@ def toggle_pause():
broker: str
'''
if "X-API-KEY" in request.headers and request.headers.get("X-API-KEY") in valid_keys:
valid_brokers = [str(item.connector) for item in earners]
if request.json is None:
return jsonify({'Error': 'request.json is None'})
broker = request.json["broker"]
if broker is None:
return jsonify({'Error': 'broker is None'})
if broker not in valid_brokers:
if broker not in valid_broker_list:
return jsonify({'Error': 'broker not valid'})
for item in earners:
if str(item.connector)==broker:
item.toggle_pause()
return jsonify({'Status': item.get_is_paused()})
return jsonify({'Error': 'broker not found'})
return jsonify({'Error': 'API key not valid'}), 401
@earn_api.route("/get_step_size", methods=["GET"])
@require_api_key
def get_step_size():
'''
GET request
@ -151,31 +193,27 @@ def get_step_size():
broker: str
'''
if "X-API-KEY" in request.headers and request.headers.get("X-API-KEY") in valid_keys:
valid_brokers = [str(item.connector) for item in earners]
broker = request.args.get("broker")
if broker is None:
return jsonify({'Error': 'broker is None'})
if broker not in valid_brokers:
if broker not in valid_broker_list:
return jsonify({'Error': 'broker not valid'})
for item in earners:
if str(item.connector)==broker:
return jsonify({'step_size': item.get_step_size()})
return jsonify({'Error': 'broker not found'})
return jsonify({'Error': 'API key not valid'}), 401
@earn_api.route("/set_step_size", methods=["POST"])
@require_api_key
def set_step_size():
if "X-API-KEY" in request.headers and request.headers.get("X-API-KEY") in valid_keys:
valid_brokers = [str(item.connector) for item in earners]
if request.json is None:
return jsonify({'Error': 'request.json is None'})
broker = request.json["broker"]
new_step_size = request.json["new_step_size"]
if broker is None:
return jsonify({'Error': 'broker is None'})
if broker not in valid_brokers:
if broker not in valid_broker_list:
return jsonify({'Error': 'broker not valid'})
if new_step_size is None:
return jsonify({'Error': 'new_step_size is None'})
@ -184,36 +222,32 @@ def set_step_size():
item.set_step_size(new_step_size)
return jsonify({'step_size': new_step_size})
return jsonify({'Error': 'broker not found'})
return jsonify({'Error': 'API key not valid'}), 401
@earn_api.route("/get_percentage", methods=["GET"])
@require_api_key
def get_percentage():
if "X-API-KEY" in request.headers and request.headers.get("X-API-KEY") in valid_keys:
valid_brokers = [str(item.connector) for item in earners]
broker = request.args.get("broker")
if broker is None:
return jsonify({'Error': 'broker is None'})
if broker not in valid_brokers:
if broker not in valid_broker_list:
return jsonify({'Error': 'broker not valid'})
for item in earners:
if str(item.connector)==broker:
return jsonify({'percentage': item.get_percentage()})
return jsonify({'Error': 'broker not found'})
return jsonify({'Error': 'API key not valid'}), 401
@earn_api.route("/set_percentage", methods=["POST"])
@require_api_key
def set_percentage():
if "X-API-KEY" in request.headers and request.headers.get("X-API-KEY") in valid_keys:
valid_brokers = [str(item.connector) for item in earners]
if request.json is None:
return jsonify({'Error': 'request.json is None'})
broker = request.json["broker"]
new_percentage = request.json["new_percentage"]
if broker is None:
return jsonify({'Error': 'broker is None'})
if broker not in valid_brokers:
if broker not in valid_broker_list:
return jsonify({'Error': 'broker not valid'})
if new_percentage is None:
return jsonify({'Error': 'new_step_size is None'})
@ -222,172 +256,211 @@ def set_percentage():
item.set_percentage(new_percentage)
return jsonify({'percentage': new_percentage})
return jsonify({'Error': 'broker not found'})
return jsonify({'Error': 'API key not valid'}), 401
@earn_api.route("/get_time_between_subscriptions", methods=["GET"])
@require_api_key
def get_time_between_subscriptions():
if "X-API-KEY" in request.headers and request.headers.get("X-API-KEY") in valid_keys:
valid_brokers = [str(item.connector) for item in earners]
broker = request.args.get("broker")
if broker is None:
return jsonify({'Error': 'broker is None'})
if broker not in valid_brokers:
if broker not in valid_broker_list:
return jsonify({'Error': 'broker not valid'})
for item in earners:
if str(item.connector)==broker:
return jsonify({'time_between_subscriptions': item.get_time_between_subscriptions()})
return jsonify({'Error': 'broker not found'})
return jsonify({'Error': 'API key not valid'}), 401
@earn_api.route("/set_time_between_subscriptions", methods=["POST"])
@require_api_key
def set_time_between_subscriptions():
if "X-API-KEY" in request.headers and request.headers.get("X-API-KEY") in valid_keys:
valid_brokers = [str(item.connector) for item in earners]
if request.json is None:
return jsonify({'Error': 'request.json is None'})
broker = request.json["broker"]
new_time_between_subscriptions = request.json["new_time_between_subscriptions"]
if broker is None:
return jsonify({'Error': 'broker is None'})
if broker not in valid_brokers:
if broker not in valid_broker_list:
return jsonify({'Error': 'broker not valid'})
if new_time_between_subscriptions is None:
return jsonify({'Error': 'new_step_size is None'})
for item in earners:
if str(item.connector)==broker:
item.set_time_between_subscriptions(new_time_between_subscriptions)
return jsonify({'percentage': new_time_between_subscriptions})
return jsonify({'time_between_subscriptions': new_time_between_subscriptions})
return jsonify({'Error': 'broker not found'})
return jsonify({'Error': 'API key not valid'}), 401
@earn_api.route("/get_time_between_redemptions", methods=["GET"])
@require_api_key
def get_time_between_redemptions():
if "X-API-KEY" in request.headers and request.headers.get("X-API-KEY") in valid_keys:
valid_brokers = [str(item.connector) for item in earners]
broker = request.args.get("broker")
if broker is None:
return jsonify({'Error': 'broker is None'})
if broker not in valid_brokers:
if broker not in valid_broker_list:
return jsonify({'Error': 'broker not valid'})
for item in earners:
if str(item.connector)==broker:
return jsonify({'time_between_redemptions': item.get_time_between_redemptions()})
return jsonify({'Error': 'broker not found'})
return jsonify({'Error': 'API key not valid'}), 401
@earn_api.route("/set_time_between_redemptions", methods=["POST"])
@require_api_key
def set_time_between_redemptions():
if "X-API-KEY" in request.headers and request.headers.get("X-API-KEY") in valid_keys:
valid_brokers = [str(item.connector) for item in earners]
if request.json is None:
return jsonify({'Error': 'request.json is None'})
broker = request.json["broker"]
new_time_between_redemptions = request.json["new_time_between_redemptions"]
if broker is None:
return jsonify({'Error': 'broker is None'})
if broker not in valid_brokers:
if broker not in valid_broker_list:
return jsonify({'Error': 'broker not valid'})
if new_time_between_redemptions is None:
return jsonify({'Error': 'new_step_size is None'})
for item in earners:
if str(item.connector)==broker:
item.set_time_between_redemptions(new_time_between_redemptions)
return jsonify({'percentage': new_time_between_redemptions})
return jsonify({'time_between_redemptions': new_time_between_redemptions})
return jsonify({'Error': 'broker not found'})
return jsonify({'Error': 'API key not valid'}), 401
@earn_api.route("/get_minimum_amount_in_trading_account", methods=["GET"])
@require_api_key
def get_minimum_amount_in_trading_account():
if "X-API-KEY" in request.headers and request.headers.get("X-API-KEY") in valid_keys:
valid_brokers = [str(item.connector) for item in earners]
broker = request.args.get("broker")
if broker is None:
return jsonify({'Error': 'broker is None'})
if broker not in valid_brokers:
if broker not in valid_broker_list:
return jsonify({'Error': 'broker not valid'})
for item in earners:
if str(item.connector)==broker:
return jsonify({'minimum_amount_in_trading_account': item.get_minimum_amount_in_trading_account()})
return jsonify({'Error': 'broker not found'})
return jsonify({'Error': 'API key not valid'}), 401
@earn_api.route("/set_minimum_amount_in_trading_account", methods=["POST"])
@require_api_key
def set_minimum_amount_in_trading_account():
if "X-API-KEY" in request.headers and request.headers.get("X-API-KEY") in valid_keys:
valid_brokers = [str(item.connector) for item in earners]
broker = request.json["broker"]
new_minimum_amount_in_trading_account = request.json["new_minimum_amount_in_trading_account"]
if broker is None:
return jsonify({'Error': 'broker is None'})
if broker not in valid_brokers:
if broker not in valid_broker_list:
return jsonify({'Error': 'broker not valid'})
if new_minimum_amount_in_trading_account is None:
return jsonify({'Error': 'new_step_size is None'})
for item in earners:
if str(item.connector)==broker:
item.set_minimum_amount_in_trading_account(new_minimum_amount_in_trading_account)
return jsonify({'percentage': new_minimum_amount_in_trading_account})
return jsonify({'minimum_amount_in_trading_account': new_minimum_amount_in_trading_account})
return jsonify({'Error': 'broker not found'})
return jsonify({'Error': 'API key not valid'}), 401
@earn_api.route("/get_last_subscription", methods=["GET"])
@require_api_key
def get_last_subscription():
if "X-API-KEY" in request.headers and request.headers.get("X-API-KEY") in valid_keys:
valid_brokers = [str(item.connector) for item in earners]
broker = request.args.get("broker")
if broker is None:
return jsonify({'Error': 'broker is None'})
if broker not in valid_brokers:
if broker not in valid_broker_list:
return jsonify({'Error': 'broker not valid'})
for item in earners:
if str(item.connector)==broker:
return jsonify({'last_subscription': item.get_last_subscription()})
return jsonify({'Error': 'broker not found'})
return jsonify({'Error': 'API key not valid'}), 401
@earn_api.route("/get_last_redemption", methods=["GET"])
@require_api_key
def get_last_redemption():
if "X-API-KEY" in request.headers and request.headers.get("X-API-KEY") in valid_keys:
valid_brokers = [str(item.connector) for item in earners]
broker = request.args.get("broker")
if broker is None:
return jsonify({'Error': 'broker is None'})
if broker not in valid_brokers:
if broker not in valid_broker_list:
return jsonify({'Error': 'broker not valid'})
for item in earners:
if str(item.connector)==broker:
return jsonify({'last_redemption': item.get_last_redemption()})
return jsonify({'Error': 'broker not found'})
return jsonify({'Error': 'API key not valid'}), 401
@earn_api.route("/get_total_balance", methods=["GET"])
@require_api_key
def get_total_balance():
if "X-API-KEY" in request.headers and request.headers.get("X-API-KEY") in valid_keys:
valid_brokers = [str(item.connector) for item in earners]
broker = request.args.get("broker")
if broker is None:
return jsonify({'Error': 'broker is None'})
if broker not in valid_brokers:
if broker not in valid_broker_list:
return jsonify({'Error': 'broker not valid'})
for item in earners:
if str(item.connector)==broker:
return jsonify({'trading_balance': item.get_trading_balance(), 'earning_balance': item.get_earning_balance()})
return jsonify({'Error': 'broker not found'})
return jsonify({'Error': 'API key not valid'}), 401
@earn_api.route("/subscribe", methods=["POST"])
@require_api_key
def subscribe():
'''
Missing endpoints:
/get_total_balance
args:
broker: broker name
amount: amount to subscribe
force_pause: True or False
'''
if request.json is None:
return jsonify({'Error': 'request.json is None'})
broker = request.json["broker"]
amount = request.json["amount"]
force_pause = False
if "force_pause" in request.json:
force_pause = request.json["force_pause"]
if broker is None:
return jsonify({'Error': 'broker is None'})
if broker not in valid_broker_list:
return jsonify({'Error': 'broker not valid'})
if amount is None:
return jsonify({'Error': 'amount is None'})
if force_pause is None:
return jsonify({'Error': 'force_pause is None'})
for item in earners:
if str(item.connector)==broker:
subscription = item.subscribe(amount, force_pause=force_pause)
return jsonify({'Success': str(subscription)})
return jsonify({'Error': 'broker not found'})
@earn_api.route("/redeem", methods=["POST"])
@require_api_key
def redeem():
'''
args:
broker: broker name
amount: amount to redeem
force_pause: True or False
'''
if request.json is None:
return jsonify({'Error': 'request.json is None'})
broker = request.json["broker"]
amount = request.json["amount"]
force_pause = False
if "force_pause" in request.json:
force_pause = request.json["force_pause"]
if broker is None:
return jsonify({'Error': 'broker is None'})
if broker not in valid_broker_list:
return jsonify({'Error': 'broker not valid'})
if amount is None:
return jsonify({'Error': 'amount is None'})
if force_pause is None:
return jsonify({'Error': 'force_pause is None'})
for item in earners:
if str(item.connector)==broker:
redemption = item.redeem(amount, force_pause=force_pause)
return jsonify({'Success': str(redemption)})
return jsonify({'Error': 'broker not found'})
def run_API(port):
@ -395,9 +468,22 @@ def run_API(port):
#earn_api.run(host="0.0.0.0", port=port)
executor = None
#Shutdown handler
def shutdown_handler(signum, _):
print(f"Received signal {signum}, shutting down as gracefully as possible...")
if executor:
executor.shutdown(wait=True, timeout=5)
os._exit(0)
# Register signals for shutdown handler
signal.signal(signal.SIGINT, shutdown_handler)
signal.signal(signal.SIGTERM, shutdown_handler)
if __name__=="__main__":
version = "2025.01.08"
version = "2025.11.20"
start_time = time.time()
with open("config.json") as f:
@ -412,15 +498,23 @@ if __name__=="__main__":
for item in config["exchanges"]:
earners.append(earner(connectors[item], config["exchanges"][item]))
#Valid broker list
valid_broker_list = [str(item.connector) for item in earners]
#Load valid API keys
valid_keys = load_keys_from_db("keys/api_credentials.db")
#Threads to run: main loop and flask api
main_threads = [Thread(target=main),Thread(target=run_API, args=(config["api_port"],))]
api_thread = Thread(target=run_API, args=(config["api_port"],), daemon=True)
#Iterate indefinitely:
for m in main_threads:
m.start()
api_thread.start()
try:
main()
except KeyboardInterrupt:
api_thread.join()
shutdown_handler(signal.SIGINT, None)

33
profits.py Normal file
View File

@ -0,0 +1,33 @@
from libraries.wrappers import earn_binance
from libraries.wrappers import earn_okx
from libraries.wrappers import earn_gateio
from libraries.balance_accounts import balance_accounts
from decimal import Decimal
binance = earn_binance.binance_earn()
gateio = earn_gateio.gateio_earn()
okx = earn_okx.okx_earn()
total_profits = []
print("Profits OKX:")
total_rewards = Decimal(0)
rewards = okx.get_lending_history("USDT")
for item in rewards["data"]:
total_rewards += Decimal(item["earnings"])
print(total_rewards)
total_profits.append(total_rewards)
print("Profits Gate.io:")
total_rewards = gateio.get_rewards_history("USDT")["interest"]
print(total_rewards)
total_profits.append(Decimal(total_rewards))
print("Profits Binance:")
total_rewards = Decimal(0)
rewards = binance.get_rewards_history()
for item in rewards["rows"]:
total_rewards += Decimal(item["rewards"])
print(total_rewards)
total_profits.append(total_rewards)
print(f"Total: {sum(total_profits)}")