From fd2b24e72e5bf39379d4453606c6d6b9eee00430 Mon Sep 17 00:00:00 2001 From: Logan Vimalaraj Date: Tue, 12 May 2026 00:54:26 -0700 Subject: [PATCH 1/2] Add Binance Spot webhook support --- README.md | 6 ++- app.py | 96 ++++++++++++++++++++++++++++------------- binanceFutures.py | 78 +++++++++++++++++---------------- binanceSpot.py | 86 +++++++++++++++++++++++++++++++++++++ config.json | 6 +++ test_binance_spot.py | 100 +++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 306 insertions(+), 66 deletions(-) create mode 100644 binanceSpot.py create mode 100644 test_binance_spot.py diff --git a/README.md b/README.md index e070ead..923ebef 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ https://discord.gg/Qb9unmxD6D #### Current Exchanges - [Bybit](https://partner.bybit.com/b/webhookbot) - [Binance Futures](https://www.binance.com/en/register?ref=LMFD8MJ5) +- [Binance Spot](https://www.binance.com/en/register?ref=LMFD8MJ5) - More will be done on request or can be added by submitting a pull request.
@@ -22,6 +23,8 @@ https://discord.gg/Qb9unmxD6D [Create Binance Futures Account](https://www.binance.com/en/register?ref=LMFD8MJ5) +[Create Binance Spot Account](https://www.binance.com/en/register?ref=LMFD8MJ5) +

@@ -119,7 +122,7 @@ _Now when your alerts fire off they should go strait to your server and get proc | Constant |Settings Keys | |--|--| |key| unique key that protects your webhook server| -|exchange | bybit, binacne-futures | +|exchange | bybit, binance-futures, binance-spot | |symbol | Exchange Specific ** See Below for more | |side|Buy or Sell | |type | Market or Limit | @@ -138,3 +141,4 @@ _Now when your alerts fire off they should go strait to your server and get proc |BYBIT INVERSE| BTCUSD| |BYBIT PERP | BTCUSDT| |Binance Futures | BTC/USDT| +|Binance Spot | BTC/USDT| diff --git a/app.py b/app.py index d2a1b00..a59557f 100644 --- a/app.py +++ b/app.py @@ -3,7 +3,8 @@ from pybit import HTTP import time import ccxt -from binanceFutures import Bot +from binanceFutures import Bot as BinanceFuturesBot +from binanceSpot import Bot as BinanceSpotBot def validate_bybit_api_key(session): try: @@ -46,24 +47,47 @@ def validate_binance_api_key(exchange): ) use_binance_futures = False +binance_futures_exchange = None if 'BINANCE-FUTURES' in config['EXCHANGES']: if config['EXCHANGES']['BINANCE-FUTURES']['ENABLED']: - print("Binance is enabled!") + print("Binance Futures is enabled!") use_binance_futures = True - exchange = ccxt.binance({ - 'apiKey': config['EXCHANGES']['BINANCE-FUTURES']['API_KEY'], - 'secret': config['EXCHANGES']['BINANCE-FUTURES']['API_SECRET'], - 'options': { - 'defaultType': 'future', + binance_futures_options = { + 'apiKey': config['EXCHANGES']['BINANCE-FUTURES']['API_KEY'], + 'secret': config['EXCHANGES']['BINANCE-FUTURES']['API_SECRET'], + 'options': { + 'defaultType': 'future', + }, + } + if config['EXCHANGES']['BINANCE-FUTURES'].get('TESTNET'): + binance_futures_options['urls'] = { + 'api': { + 'public': 'https://testnet.binancefuture.com/fapi/v1', + 'private': 'https://testnet.binancefuture.com/fapi/v1', + }, + } + + binance_futures_exchange = ccxt.binance(binance_futures_options) + if config['EXCHANGES']['BINANCE-FUTURES'].get('TESTNET'): + binance_futures_exchange.set_sandbox_mode(True) + +use_binance_spot = False +binance_spot_exchange = None +if 'BINANCE-SPOT' in config['EXCHANGES']: + if config['EXCHANGES']['BINANCE-SPOT']['ENABLED']: + print("Binance Spot is enabled!") + use_binance_spot = True + + binance_spot_exchange = ccxt.binance({ + 'apiKey': config['EXCHANGES']['BINANCE-SPOT']['API_KEY'], + 'secret': config['EXCHANGES']['BINANCE-SPOT']['API_SECRET'], + 'options': { + 'defaultType': 'spot', }, - 'urls': { - 'api': { - 'public': 'https://testnet.binancefuture.com/fapi/v1', - 'private': 'https://testnet.binancefuture.com/fapi/v1', - }, } }) - exchange.set_sandbox_mode(True) + if config['EXCHANGES']['BINANCE-SPOT'].get('TESTNET'): + binance_spot_exchange.set_sandbox_mode(True) # Validate Bybit API key if use_bybit: @@ -73,10 +97,16 @@ def validate_binance_api_key(exchange): # Validate Binance Futures API key if use_binance_futures: - if not validate_binance_api_key(exchange): + if not validate_binance_api_key(binance_futures_exchange): print("Invalid Binance Futures API key.") use_binance_futures = False +# Validate Binance Spot API key +if use_binance_spot: + if not validate_binance_api_key(binance_spot_exchange): + print("Invalid Binance Spot API key.") + use_binance_spot = False + @app.route('/') def index(): return {'message': 'Server is running!'} @@ -170,23 +200,31 @@ def webhook(): ############################################################################## # Binance Futures ############################################################################## - if data['exchange'] == 'binance-futures': - if use_binance_futures: - bot = Bot() - bot.run(data) - return { - "status": "success", - "message": "Binance Futures Webhook Received!" - } - - else: - print("Invalid Exchange, Please Try Again!") + if data['exchange'] == 'binance-futures': + if use_binance_futures: + bot = BinanceFuturesBot(exchange_override=binance_futures_exchange) + bot.run(data) + return { + "status": "success", + "message": "Binance Futures Webhook Received!" + } + ############################################################################## + # Binance Spot + ############################################################################## + if data['exchange'] == 'binance-spot': + if use_binance_spot: + bot = BinanceSpotBot(exchange_override=binance_spot_exchange) + bot.run(data) return { - "status": "error", - "message": "Invalid Exchange, Please Try Again!" + "status": "success", + "message": "Binance Spot Webhook Received!" } + print("Invalid Exchange, Please Try Again!") + return { + "status": "error", + "message": "Invalid Exchange, Please Try Again!" + } + if __name__ == '__main__': app.run(debug=False) - - diff --git a/binanceFutures.py b/binanceFutures.py index e2034ed..136f147 100644 --- a/binanceFutures.py +++ b/binanceFutures.py @@ -8,10 +8,16 @@ config = json.load(config_file) -if config['EXCHANGES']['binance-futures']['TESTNET']: +exchange_config = ( + config['EXCHANGES'].get('BINANCE-FUTURES') + or config['EXCHANGES'].get('binance-futures') + or {} +) + +if exchange_config.get('TESTNET'): exchange = ccxt.binance({ - 'apiKey': config['EXCHANGES']['binance-futures']['API_KEY'], - 'secret': config['EXCHANGES']['binance-futures']['API_SECRET'], + 'apiKey': exchange_config.get('API_KEY', ''), + 'secret': exchange_config.get('API_SECRET', ''), 'options': { 'defaultType': 'future', }, @@ -24,8 +30,8 @@ exchange.set_sandbox_mode(True) else: exchange = ccxt.binance({ - 'apiKey': config['EXCHANGES']['binance-futures']['API_KEY'], - 'secret': config['EXCHANGES']['binance-futures']['API_SECRET'], + 'apiKey': exchange_config.get('API_KEY', ''), + 'secret': exchange_config.get('API_SECRET', ''), 'options': { 'defaultType': 'future', }, @@ -38,8 +44,8 @@ class Bot: - def __int__(self): - pass + def __init__(self, exchange_override=None): + self.exchange = exchange_override or exchange def create_string(self): N = 7 @@ -52,7 +58,7 @@ def create_string(self): return def close_position(self, symbol): - position = exchange.fetch_positions(symbol)[0]['info']['positionAmt'] + position = self.exchange.fetch_positions(symbol)[0]['info']['positionAmt'] self.create_string() params = { "newClientOrderId": self.clientId, @@ -60,41 +66,41 @@ def close_position(self, symbol): } if float(position) > 0: print("Closing Long Position") - exchange.create_order(symbol, 'Market', 'Sell', float(position), price=None, params=params) + self.exchange.create_order(symbol, 'Market', 'Sell', float(position), price=None, params=params) else: print("Closing Short Position") - exchange.create_order(symbol, 'Market', 'Buy', -float(position), price=None, params=params) + self.exchange.create_order(symbol, 'Market', 'Buy', -float(position), price=None, params=params) def set_risk(self, symbol, data, stop_loss, take_profit): - position = exchange.fetch_positions(symbol) + position = self.exchange.fetch_positions(symbol) print(position) price = float(position[0]['info']['entryPrice']) size = abs(float(position[0]['info']['positionAmt'])) - markPrice = float(exchange.fetch_ticker(data['symbol'])['last']) + markPrice = float(self.exchange.fetch_ticker(data['symbol'])['last']) if data['order_mode'] == 'Both': if data['side'] == 'Buy': self.create_string() - exchange.create_order(symbol, 'STOP_MARKET', 'Sell', size, params={ + self.exchange.create_order(symbol, 'STOP_MARKET', 'Sell', size, params={ "newClientOrderId": self.clientId, 'reduceOnly': True, 'stopPrice': stop_loss, }) self.create_string() - exchange.create_order(symbol, 'TAKE_PROFIT', 'Sell', size, params={ + self.exchange.create_order(symbol, 'TAKE_PROFIT', 'Sell', size, params={ "newClientOrderId": self.clientId, 'reduceOnly': True, 'stopPrice': take_profit, }) else: self.create_string() - exchange.create_order(symbol, 'STOP_MARKET', 'Buy', size, params={ + self.exchange.create_order(symbol, 'STOP_MARKET', 'Buy', size, params={ "newClientOrderId": self.clientId, 'reduceOnly': True, 'stopPrice': stop_loss, }) self.create_string() - exchange.create_order(symbol, 'TAKE_PROFIT', 'Buy', size, take_profit, params={ + self.exchange.create_order(symbol, 'TAKE_PROFIT', 'Buy', size, take_profit, params={ "newClientOrderId": self.clientId, 'reduceOnly': True, 'stopPrice': take_profit, @@ -103,14 +109,14 @@ def set_risk(self, symbol, data, stop_loss, take_profit): elif data['order_mode'] == 'Profit': if data['side'] == 'Buy': self.create_string() - exchange.create_order(symbol, 'TAKE_PROFIT', 'Sell', size, params={ + self.exchange.create_order(symbol, 'TAKE_PROFIT', 'Sell', size, params={ "newClientOrderId": self.clientId, 'reduceOnly': True, 'stopPrice': take_profit, }) else: self.create_string() - exchange.create_order(symbol, 'TAKE_PROFIT', 'Buy', size, take_profit, params={ + self.exchange.create_order(symbol, 'TAKE_PROFIT', 'Buy', size, take_profit, params={ "newClientOrderId": self.clientId, 'reduceOnly': True, 'stopPrice': take_profit, @@ -118,14 +124,14 @@ def set_risk(self, symbol, data, stop_loss, take_profit): elif data['order_mode'] == 'Stop': if data['side'] == 'Buy': self.create_string() - exchange.create_order(symbol, 'STOP_MARKET', 'Sell', size, params={ + self.exchange.create_order(symbol, 'STOP_MARKET', 'Sell', size, params={ "newClientOrderId": self.clientId, 'reduceOnly': True, 'stopPrice': stop_loss, }) else: self.create_string() - exchange.create_order(symbol, 'STOP_MARKET', 'Buy', size, params={ + self.exchange.create_order(symbol, 'STOP_MARKET', 'Buy', size, params={ "newClientOrderId": self.clientId, 'reduceOnly': True, 'stopPrice': stop_loss, @@ -142,7 +148,7 @@ def run(self, data): else: if 'cancel_orders' in data: print("Cancelling Order") - exchange.cancel_all_orders(symbol=data['symbol']) + self.exchange.cancel_all_orders(symbol=data['symbol']) if 'type' in data: print("Placing Order") if 'price' in data: @@ -153,7 +159,7 @@ def run(self, data): if data['order_mode'] == 'Both': take_profit_percent = float(data['take_profit_percent']) / 100 stop_loss_percent = float(data['stop_loss_percent']) / 100 - current_price = exchange.fetch_ticker(data['symbol'])['last'] + current_price = self.exchange.fetch_ticker(data['symbol'])['last'] if data['side'] == 'Buy': take_profit_price = round(float(current_price) + (float(current_price) * take_profit_percent), 2) @@ -172,18 +178,18 @@ def run(self, data): 'reduceOnly': False } if data['type'] == 'Limit': - exchange.create_order(data['symbol'], data['type'], data['side'], float(data['qty']), - price=float(price), params=params) + self.exchange.create_order(data['symbol'], data['type'], data['side'], float(data['qty']), + price=float(price), params=params) else: - exchange.create_order(data['symbol'], data['type'], data['side'], float(data['qty']), - params=params) + self.exchange.create_order(data['symbol'], data['type'], data['side'], float(data['qty']), + params=params) self.set_risk(data['symbol'], data, stop_loss_price, take_profit_price) elif data['order_mode'] == 'Profit': take_profit_percent = float(data['take_profit_percent']) / 100 - current_price = exchange.fetch_ticker(data['symbol'])['last'] + current_price = self.exchange.fetch_ticker(data['symbol'])['last'] if data['side'] == 'Buy': take_profit_price = round(float(current_price) + (float(current_price) * take_profit_percent), @@ -201,18 +207,18 @@ def run(self, data): } if data['type'] == 'Limit': - exchange.create_order(data['symbol'], data['type'], data['side'], float(data['qty']), - price=float(price), params=params) + self.exchange.create_order(data['symbol'], data['type'], data['side'], float(data['qty']), + price=float(price), params=params) else: - exchange.create_order(data['symbol'], data['type'], data['side'], float(data['qty']), - params=params) + self.exchange.create_order(data['symbol'], data['type'], data['side'], float(data['qty']), + params=params) self.set_risk(data['symbol'], data, 0, take_profit_price) elif data['order_mode'] == 'Stop': stop_loss_percent = float(data['stop_loss_percent']) / 100 - current_price = exchange.fetch_ticker(data['symbol'])['last'] + current_price = self.exchange.fetch_ticker(data['symbol'])['last'] if data['side'] == 'Buy': stop_loss_price = round(float(current_price) - (float(current_price) * stop_loss_percent), 2) @@ -228,11 +234,11 @@ def run(self, data): } if data['type'] == 'Limit': - exchange.create_order(data['symbol'], data['type'], data['side'], float(data['qty']), - price=float(price), params=params) + self.exchange.create_order(data['symbol'], data['type'], data['side'], float(data['qty']), + price=float(price), params=params) else: - exchange.create_order(data['symbol'], data['type'], data['side'], float(data['qty']), - params=params) + self.exchange.create_order(data['symbol'], data['type'], data['side'], float(data['qty']), + params=params) self.set_risk(data['symbol'], data, stop_loss_price, 0) diff --git a/binanceSpot.py b/binanceSpot.py new file mode 100644 index 0000000..01007f4 --- /dev/null +++ b/binanceSpot.py @@ -0,0 +1,86 @@ +import json +import random +import string + +import ccxt + +with open('config.json') as config_file: + config = json.load(config_file) + + +exchange_config = ( + config['EXCHANGES'].get('BINANCE-SPOT') + or config['EXCHANGES'].get('binance-spot') + or {} +) + +exchange = ccxt.binance({ + 'apiKey': exchange_config.get('API_KEY', ''), + 'secret': exchange_config.get('API_SECRET', ''), + 'options': { + 'defaultType': 'spot', + }, +}) + +if exchange_config.get('TESTNET'): + exchange.set_sandbox_mode(True) + + +class Bot: + + def __init__(self, exchange_override=None): + self.exchange = exchange_override or exchange + + def create_string(self): + token = ''.join(random.choices(string.ascii_uppercase + string.digits, k=7)) + self.clientId = 'x-40PTWbMI' + token + + def close_position(self, symbol): + base_currency = symbol.split('/')[0] + balance = self.exchange.fetch_balance() + free_balance = balance.get(base_currency, {}).get('free') + + if free_balance is None: + free_balance = balance.get('free', {}).get(base_currency, 0) + + amount = float(free_balance or 0) + if amount <= 0: + print("No Binance Spot balance available to close for " + base_currency) + return None + + self.create_string() + return self.exchange.create_order( + symbol, + 'market', + 'sell', + amount, + params={"newClientOrderId": self.clientId} + ) + + def run(self, data): + if data['close_position'] == 'True': + print("Closing Binance Spot position") + return self.close_position(symbol=data['symbol']) + + if 'cancel_orders' in data: + print("Cancelling Binance Spot orders") + self.exchange.cancel_all_orders(symbol=data['symbol']) + + if 'type' not in data: + return {'status': 'success'} + + print("Placing Binance Spot order") + self.create_string() + order_type = data['type'].lower() + side = data['side'].lower() + price = float(data['price']) if order_type == 'limit' and 'price' in data else None + + params = {"newClientOrderId": self.clientId} + return self.exchange.create_order( + data['symbol'], + order_type, + side, + float(data['qty']), + price=price, + params=params + ) diff --git a/config.json b/config.json index c102c7d..5f0319b 100644 --- a/config.json +++ b/config.json @@ -12,6 +12,12 @@ "API_SECRET": "api-secret-goes-here", "ENABLED": false, "TESTNET": false + }, + "BINANCE-SPOT": { + "API_KEY": "api-key-goes-here", + "API_SECRET": "api-secret-goes-here", + "ENABLED": false, + "TESTNET": false } } diff --git a/test_binance_spot.py b/test_binance_spot.py new file mode 100644 index 0000000..9454d8c --- /dev/null +++ b/test_binance_spot.py @@ -0,0 +1,100 @@ +import unittest + +from binanceSpot import Bot + + +class FakeExchange: + def __init__(self): + self.orders = [] + self.cancelled_symbols = [] + self.balance = {'BTC': {'free': 0.25}} + + def create_order(self, symbol, order_type, side, amount, price=None, params=None): + order = { + 'symbol': symbol, + 'type': order_type, + 'side': side, + 'amount': amount, + 'price': price, + 'params': params or {}, + } + self.orders.append(order) + return order + + def cancel_all_orders(self, symbol): + self.cancelled_symbols.append(symbol) + + def fetch_balance(self): + return self.balance + + +class BinanceSpotBotTest(unittest.TestCase): + def test_places_market_order(self): + exchange = FakeExchange() + bot = Bot(exchange_override=exchange) + + bot.run({ + 'symbol': 'BTC/USDT', + 'type': 'Market', + 'side': 'Buy', + 'qty': '0.1', + 'close_position': 'False', + }) + + self.assertEqual(exchange.orders[0]['symbol'], 'BTC/USDT') + self.assertEqual(exchange.orders[0]['type'], 'market') + self.assertEqual(exchange.orders[0]['side'], 'buy') + self.assertEqual(exchange.orders[0]['amount'], 0.1) + self.assertIsNone(exchange.orders[0]['price']) + self.assertIn('newClientOrderId', exchange.orders[0]['params']) + + def test_places_limit_order_with_price(self): + exchange = FakeExchange() + bot = Bot(exchange_override=exchange) + + bot.run({ + 'symbol': 'ETH/USDT', + 'type': 'Limit', + 'side': 'Sell', + 'qty': '1.5', + 'price': '2500.50', + 'close_position': 'False', + }) + + self.assertEqual(exchange.orders[0]['type'], 'limit') + self.assertEqual(exchange.orders[0]['side'], 'sell') + self.assertEqual(exchange.orders[0]['amount'], 1.5) + self.assertEqual(exchange.orders[0]['price'], 2500.5) + + def test_cancels_open_orders_before_placing_order(self): + exchange = FakeExchange() + bot = Bot(exchange_override=exchange) + + bot.run({ + 'symbol': 'BTC/USDT', + 'type': 'Market', + 'side': 'Buy', + 'qty': '0.1', + 'close_position': 'False', + 'cancel_orders': 'True', + }) + + self.assertEqual(exchange.cancelled_symbols, ['BTC/USDT']) + self.assertEqual(len(exchange.orders), 1) + + def test_close_position_sells_available_base_balance(self): + exchange = FakeExchange() + bot = Bot(exchange_override=exchange) + + bot.run({ + 'symbol': 'BTC/USDT', + 'close_position': 'True', + }) + + self.assertEqual(exchange.orders[0]['type'], 'market') + self.assertEqual(exchange.orders[0]['side'], 'sell') + self.assertEqual(exchange.orders[0]['amount'], 0.25) + + +if __name__ == '__main__': + unittest.main() From 9c887ea0ebd65e217ea4ed0c1100f681b5ea2c57 Mon Sep 17 00:00:00 2001 From: Logan Vimalaraj Date: Tue, 12 May 2026 01:00:36 -0700 Subject: [PATCH 2/2] Add KuCoin webhook support --- README.md | 4 +- app.py | 41 ++++++++++++++++++-- config.json | 7 ++++ kucoin.py | 79 ++++++++++++++++++++++++++++++++++++++ test_kucoin.py | 100 +++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 226 insertions(+), 5 deletions(-) create mode 100644 kucoin.py create mode 100644 test_kucoin.py diff --git a/README.md b/README.md index 923ebef..ee9a784 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ https://discord.gg/Qb9unmxD6D - [Bybit](https://partner.bybit.com/b/webhookbot) - [Binance Futures](https://www.binance.com/en/register?ref=LMFD8MJ5) - [Binance Spot](https://www.binance.com/en/register?ref=LMFD8MJ5) +- [KuCoin](https://www.kucoin.com/) - More will be done on request or can be added by submitting a pull request.
@@ -122,7 +123,7 @@ _Now when your alerts fire off they should go strait to your server and get proc | Constant |Settings Keys | |--|--| |key| unique key that protects your webhook server| -|exchange | bybit, binance-futures, binance-spot | +|exchange | bybit, binance-futures, binance-spot, kucoin | |symbol | Exchange Specific ** See Below for more | |side|Buy or Sell | |type | Market or Limit | @@ -142,3 +143,4 @@ _Now when your alerts fire off they should go strait to your server and get proc |BYBIT PERP | BTCUSDT| |Binance Futures | BTC/USDT| |Binance Spot | BTC/USDT| +|KuCoin | BTC/USDT| diff --git a/app.py b/app.py index a59557f..759f324 100644 --- a/app.py +++ b/app.py @@ -5,6 +5,7 @@ import ccxt from binanceFutures import Bot as BinanceFuturesBot from binanceSpot import Bot as BinanceSpotBot +from kucoin import Bot as KuCoinBot def validate_bybit_api_key(session): try: @@ -14,12 +15,12 @@ def validate_bybit_api_key(session): print("Bybit API key validation failed:", str(e)) return False -def validate_binance_api_key(exchange): +def validate_exchange_api_key(exchange, exchange_name): try: result = exchange.fetch_balance() return True except Exception as e: - print("Binance API key validation failed:", str(e)) + print(exchange_name + " API key validation failed:", str(e)) return False app = Flask(__name__) @@ -97,16 +98,37 @@ def validate_binance_api_key(exchange): # Validate Binance Futures API key if use_binance_futures: - if not validate_binance_api_key(binance_futures_exchange): + if not validate_exchange_api_key(binance_futures_exchange, "Binance Futures"): print("Invalid Binance Futures API key.") use_binance_futures = False # Validate Binance Spot API key if use_binance_spot: - if not validate_binance_api_key(binance_spot_exchange): + if not validate_exchange_api_key(binance_spot_exchange, "Binance Spot"): print("Invalid Binance Spot API key.") use_binance_spot = False +use_kucoin = False +kucoin_exchange = None +if 'KUCOIN' in config['EXCHANGES']: + if config['EXCHANGES']['KUCOIN']['ENABLED']: + print("KuCoin is enabled!") + use_kucoin = True + + kucoin_exchange = ccxt.kucoin({ + 'apiKey': config['EXCHANGES']['KUCOIN']['API_KEY'], + 'secret': config['EXCHANGES']['KUCOIN']['API_SECRET'], + 'password': config['EXCHANGES']['KUCOIN']['API_PASSPHRASE'], + }) + if config['EXCHANGES']['KUCOIN'].get('TESTNET'): + kucoin_exchange.set_sandbox_mode(True) + +# Validate KuCoin API key +if use_kucoin: + if not validate_exchange_api_key(kucoin_exchange, "KuCoin"): + print("Invalid KuCoin API key.") + use_kucoin = False + @app.route('/') def index(): return {'message': 'Server is running!'} @@ -219,6 +241,17 @@ def webhook(): "status": "success", "message": "Binance Spot Webhook Received!" } + ############################################################################## + # KuCoin + ############################################################################## + if data['exchange'] == 'kucoin': + if use_kucoin: + bot = KuCoinBot(exchange_override=kucoin_exchange) + bot.run(data) + return { + "status": "success", + "message": "KuCoin Webhook Received!" + } print("Invalid Exchange, Please Try Again!") return { diff --git a/config.json b/config.json index 5f0319b..139eecb 100644 --- a/config.json +++ b/config.json @@ -18,6 +18,13 @@ "API_SECRET": "api-secret-goes-here", "ENABLED": false, "TESTNET": false + }, + "KUCOIN": { + "API_KEY": "api-key-goes-here", + "API_SECRET": "api-secret-goes-here", + "API_PASSPHRASE": "api-passphrase-goes-here", + "ENABLED": false, + "TESTNET": false } } diff --git a/kucoin.py b/kucoin.py new file mode 100644 index 0000000..799354c --- /dev/null +++ b/kucoin.py @@ -0,0 +1,79 @@ +import json +import random +import string + +import ccxt + +with open('config.json') as config_file: + config = json.load(config_file) + + +exchange_config = config['EXCHANGES'].get('KUCOIN') or {} + +exchange = ccxt.kucoin({ + 'apiKey': exchange_config.get('API_KEY', ''), + 'secret': exchange_config.get('API_SECRET', ''), + 'password': exchange_config.get('API_PASSPHRASE', ''), +}) + +if exchange_config.get('TESTNET'): + exchange.set_sandbox_mode(True) + + +class Bot: + + def __init__(self, exchange_override=None): + self.exchange = exchange_override or exchange + + def create_string(self): + token = ''.join(random.choices(string.ascii_uppercase + string.digits, k=7)) + self.clientId = 'x-40PTWbMI' + token + + def close_position(self, symbol): + base_currency = symbol.split('/')[0] + balance = self.exchange.fetch_balance() + free_balance = balance.get(base_currency, {}).get('free') + + if free_balance is None: + free_balance = balance.get('free', {}).get(base_currency, 0) + + amount = float(free_balance or 0) + if amount <= 0: + print("No KuCoin balance available to close for " + base_currency) + return None + + self.create_string() + return self.exchange.create_order( + symbol, + 'market', + 'sell', + amount, + params={"clientOid": self.clientId} + ) + + def run(self, data): + if data['close_position'] == 'True': + print("Closing KuCoin position") + return self.close_position(symbol=data['symbol']) + + if 'cancel_orders' in data: + print("Cancelling KuCoin orders") + self.exchange.cancel_all_orders(symbol=data['symbol']) + + if 'type' not in data: + return {'status': 'success'} + + print("Placing KuCoin order") + self.create_string() + order_type = data['type'].lower() + side = data['side'].lower() + price = float(data['price']) if order_type == 'limit' and 'price' in data else None + + return self.exchange.create_order( + data['symbol'], + order_type, + side, + float(data['qty']), + price=price, + params={"clientOid": self.clientId} + ) diff --git a/test_kucoin.py b/test_kucoin.py new file mode 100644 index 0000000..6d8ae9f --- /dev/null +++ b/test_kucoin.py @@ -0,0 +1,100 @@ +import unittest + +from kucoin import Bot + + +class FakeExchange: + def __init__(self): + self.orders = [] + self.cancelled_symbols = [] + self.balance = {'KCS': {'free': 12}} + + def create_order(self, symbol, order_type, side, amount, price=None, params=None): + order = { + 'symbol': symbol, + 'type': order_type, + 'side': side, + 'amount': amount, + 'price': price, + 'params': params or {}, + } + self.orders.append(order) + return order + + def cancel_all_orders(self, symbol): + self.cancelled_symbols.append(symbol) + + def fetch_balance(self): + return self.balance + + +class KuCoinBotTest(unittest.TestCase): + def test_places_market_order(self): + exchange = FakeExchange() + bot = Bot(exchange_override=exchange) + + bot.run({ + 'symbol': 'KCS/USDT', + 'type': 'Market', + 'side': 'Buy', + 'qty': '2', + 'close_position': 'False', + }) + + self.assertEqual(exchange.orders[0]['symbol'], 'KCS/USDT') + self.assertEqual(exchange.orders[0]['type'], 'market') + self.assertEqual(exchange.orders[0]['side'], 'buy') + self.assertEqual(exchange.orders[0]['amount'], 2) + self.assertIsNone(exchange.orders[0]['price']) + self.assertIn('clientOid', exchange.orders[0]['params']) + + def test_places_limit_order_with_price(self): + exchange = FakeExchange() + bot = Bot(exchange_override=exchange) + + bot.run({ + 'symbol': 'KCS/USDT', + 'type': 'Limit', + 'side': 'Sell', + 'qty': '3.5', + 'price': '15.25', + 'close_position': 'False', + }) + + self.assertEqual(exchange.orders[0]['type'], 'limit') + self.assertEqual(exchange.orders[0]['side'], 'sell') + self.assertEqual(exchange.orders[0]['amount'], 3.5) + self.assertEqual(exchange.orders[0]['price'], 15.25) + + def test_cancels_open_orders_before_placing_order(self): + exchange = FakeExchange() + bot = Bot(exchange_override=exchange) + + bot.run({ + 'symbol': 'KCS/USDT', + 'type': 'Market', + 'side': 'Buy', + 'qty': '2', + 'close_position': 'False', + 'cancel_orders': 'True', + }) + + self.assertEqual(exchange.cancelled_symbols, ['KCS/USDT']) + self.assertEqual(len(exchange.orders), 1) + + def test_close_position_sells_available_base_balance(self): + exchange = FakeExchange() + bot = Bot(exchange_override=exchange) + + bot.run({ + 'symbol': 'KCS/USDT', + 'close_position': 'True', + }) + + self.assertEqual(exchange.orders[0]['type'], 'market') + self.assertEqual(exchange.orders[0]['side'], 'sell') + self.assertEqual(exchange.orders[0]['amount'], 12) + + +if __name__ == '__main__': + unittest.main()