diff --git a/README.md b/README.md
index e070ead..ee9a784 100644
--- a/README.md
+++ b/README.md
@@ -10,6 +10,8 @@ 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)
+- [KuCoin](https://www.kucoin.com/)
- More will be done on request or can be added by submitting a pull request.
@@ -22,6 +24,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 +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, binacne-futures |
+|exchange | bybit, binance-futures, binance-spot, kucoin |
|symbol | Exchange Specific ** See Below for more |
|side|Buy or Sell |
|type | Market or Limit |
@@ -138,3 +142,5 @@ _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|
+|KuCoin | BTC/USDT|
diff --git a/app.py b/app.py
index d2a1b00..759f324 100644
--- a/app.py
+++ b/app.py
@@ -3,7 +3,9 @@
from pybit import HTTP
import time
import ccxt
-from binanceFutures import Bot
+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:
@@ -13,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__)
@@ -46,24 +48,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 +98,37 @@ 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_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_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!'}
@@ -170,23 +222,42 @@ 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": "error",
- "message": "Invalid Exchange, Please Try Again!"
+ "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": "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 {
+ "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..139eecb 100644
--- a/config.json
+++ b/config.json
@@ -12,6 +12,19 @@
"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
+ },
+ "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_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()
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()