diff --git a/README.md b/README.md index e070ead..476ba33 100644 --- a/README.md +++ b/README.md @@ -125,6 +125,7 @@ _Now when your alerts fire off they should go strait to your server and get proc |type | Market or Limit | |order_mode| Both(Stop Loss & Take Profit Orders Used), Profit ( Omly Take Profit Orders), Stop (Only Stop Loss orders)| |qty| amount of base currency to buy | +|qty_percent| optional percentage of available quote balance to use instead of qty on Binance Futures | |price| ticker in quote currency | |close_position| True or False | |cancel_orders|True or False | diff --git a/app.py b/app.py index d2a1b00..95b40e0 100644 --- a/app.py +++ b/app.py @@ -170,23 +170,31 @@ def webhook(): ############################################################################## # Binance Futures ############################################################################## - if data['exchange'] == 'binance-futures': - if use_binance_futures: - bot = Bot() + if data['exchange'] == 'binance-futures': + if use_binance_futures: + bot = Bot() + try: bot.run(data) - return { - "status": "success", - "message": "Binance Futures Webhook Received!" - } - - else: - print("Invalid Exchange, Please Try Again!") + except Exception as e: + return jsonify({ + "status": "error", + "message": str(e) + }), 400 return { - "status": "error", - "message": "Invalid Exchange, Please Try Again!" + "status": "success", + "message": "Binance Futures Webhook Received!" } + return { + "status": "error", + "message": "Binance Futures is not enabled or API validation failed." + } + + else: + 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..2a55f26 100644 --- a/binanceFutures.py +++ b/binanceFutures.py @@ -3,15 +3,17 @@ import ccxt import random import string +from order_sizing import calculate_qty_from_percent with open('config.json') as config_file: config = json.load(config_file) +EXCHANGE_CONFIG = config['EXCHANGES']['BINANCE-FUTURES'] -if config['EXCHANGES']['binance-futures']['TESTNET']: +if EXCHANGE_CONFIG['TESTNET']: exchange = ccxt.binance({ - 'apiKey': config['EXCHANGES']['binance-futures']['API_KEY'], - 'secret': config['EXCHANGES']['binance-futures']['API_SECRET'], + 'apiKey': EXCHANGE_CONFIG['API_KEY'], + 'secret': EXCHANGE_CONFIG['API_SECRET'], 'options': { 'defaultType': 'future', }, @@ -24,8 +26,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['API_KEY'], + 'secret': EXCHANGE_CONFIG['API_SECRET'], 'options': { 'defaultType': 'future', }, @@ -36,6 +38,15 @@ }, } }) +def get_order_reference_price(data, current_price): + if data['type'] != 'Limit': + price = float(current_price) + else: + price = float(data.get('price', 0)) + if price <= 0: + raise ValueError('Limit orders require a price greater than 0') + return price + class Bot: def __int__(self): @@ -51,6 +62,13 @@ def create_string(self): self.clientId = baseId + str(res) return + def get_order_quantity(self, data, reference_price): + if 'qty_percent' not in data: + return float(data['qty']) + balance = exchange.fetch_balance() + qty = calculate_qty_from_percent(balance, data['symbol'], reference_price, data['qty_percent']) + return float(exchange.amount_to_precision(data['symbol'], qty)) + def close_position(self, symbol): position = exchange.fetch_positions(symbol)[0]['info']['positionAmt'] self.create_string() @@ -149,11 +167,14 @@ def run(self, data): price = data['price'] else: price = 0 + if data['order_mode'] in ('Both', 'Profit', 'Stop'): + current_price = float(exchange.fetch_ticker(data['symbol'])['last']) + reference_price = get_order_reference_price(data, current_price) + qty = self.get_order_quantity(data, reference_price) 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'] if data['side'] == 'Buy': take_profit_price = round(float(current_price) + (float(current_price) * take_profit_percent), 2) @@ -172,10 +193,10 @@ 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) + exchange.create_order(data['symbol'], data['type'], data['side'], qty, + price=reference_price, params=params) else: - exchange.create_order(data['symbol'], data['type'], data['side'], float(data['qty']), + exchange.create_order(data['symbol'], data['type'], data['side'], qty, params=params) self.set_risk(data['symbol'], data, stop_loss_price, take_profit_price) @@ -183,7 +204,6 @@ def run(self, data): elif data['order_mode'] == 'Profit': take_profit_percent = float(data['take_profit_percent']) / 100 - current_price = exchange.fetch_ticker(data['symbol'])['last'] if data['side'] == 'Buy': take_profit_price = round(float(current_price) + (float(current_price) * take_profit_percent), @@ -201,10 +221,10 @@ 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) + exchange.create_order(data['symbol'], data['type'], data['side'], qty, + price=reference_price, params=params) else: - exchange.create_order(data['symbol'], data['type'], data['side'], float(data['qty']), + exchange.create_order(data['symbol'], data['type'], data['side'], qty, params=params) self.set_risk(data['symbol'], data, 0, take_profit_price) @@ -212,7 +232,6 @@ def run(self, data): elif data['order_mode'] == 'Stop': stop_loss_percent = float(data['stop_loss_percent']) / 100 - current_price = 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,10 +247,10 @@ 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) + exchange.create_order(data['symbol'], data['type'], data['side'], qty, + price=reference_price, params=params) else: - exchange.create_order(data['symbol'], data['type'], data['side'], float(data['qty']), + exchange.create_order(data['symbol'], data['type'], data['side'], qty, params=params) self.set_risk(data['symbol'], data, stop_loss_price, 0) diff --git a/order_sizing.py b/order_sizing.py new file mode 100644 index 0000000..b84421d --- /dev/null +++ b/order_sizing.py @@ -0,0 +1,43 @@ +def get_quote_currency(symbol): + if not symbol: + return 'USDT' + if '/' in symbol: + return symbol.split('/')[-1] + for quote in ('USDT', 'BUSD', 'USDC', 'USD', 'BTC', 'ETH'): + if symbol.endswith(quote): + return quote + return 'USDT' + + +def get_available_balance(balance, quote_currency): + if not balance: + return 0 + if quote_currency in balance: + if isinstance(balance[quote_currency], dict): + if balance[quote_currency].get('free') is not None: + return float(balance[quote_currency].get('free')) + return float(balance[quote_currency].get('total') or 0) + return float(balance[quote_currency] or 0) + free = balance.get('free', {}) + total = balance.get('total', {}) + if isinstance(free, dict) and quote_currency in free: + return float(free.get(quote_currency) or 0) + if isinstance(total, dict) and quote_currency in total: + return float(total.get(quote_currency) or 0) + return 0 + + +def calculate_qty_from_percent(balance, symbol, price, qty_percent): + percent = float(qty_percent) + if percent <= 0 or percent > 100: + raise ValueError('qty_percent must be greater than 0 and less than or equal to 100') + price = float(price) + if price <= 0: + raise ValueError('price must be greater than 0') + + quote_currency = get_quote_currency(symbol) + available_balance = get_available_balance(balance, quote_currency) + if available_balance <= 0: + raise ValueError(f'No available {quote_currency} balance') + + return (available_balance * (percent / 100)) / price diff --git a/test_order_sizing.py b/test_order_sizing.py new file mode 100644 index 0000000..8543163 --- /dev/null +++ b/test_order_sizing.py @@ -0,0 +1,52 @@ +import unittest +import sys +import types + +ccxt_stub = types.SimpleNamespace(binance=lambda *args, **kwargs: types.SimpleNamespace(set_sandbox_mode=lambda enabled: None)) +sys.modules.setdefault('ccxt', ccxt_stub) +from binanceFutures import get_order_reference_price +from order_sizing import calculate_qty_from_percent, get_available_balance, get_quote_currency + + +class OrderSizingTest(unittest.TestCase): + def test_get_quote_currency_from_slash_symbol(self): + self.assertEqual(get_quote_currency('BTC/USDT'), 'USDT') + + def test_get_quote_currency_from_joined_symbol(self): + self.assertEqual(get_quote_currency('ETHBUSD'), 'BUSD') + + def test_calculate_qty_from_percent_uses_free_quote_balance(self): + balance = {'USDT': {'free': 1000}} + + qty = calculate_qty_from_percent(balance, 'BTC/USDT', 25000, 10) + + self.assertAlmostEqual(qty, 0.004) + + def test_calculate_qty_from_percent_supports_ccxt_free_map(self): + balance = {'free': {'USDT': 500}} + + qty = calculate_qty_from_percent(balance, 'ETH/USDT', 2000, 25) + + self.assertAlmostEqual(qty, 0.0625) + + def test_get_available_balance_does_not_fall_back_when_free_is_zero(self): + balance = {'USDT': {'free': 0, 'total': 1000}} + + self.assertEqual(get_available_balance(balance, 'USDT'), 0) + + def test_calculate_qty_from_percent_rejects_invalid_percent(self): + with self.assertRaises(ValueError): + calculate_qty_from_percent({'USDT': {'free': 1000}}, 'BTC/USDT', 25000, 101) + + def test_get_order_reference_price_uses_numeric_limit_price(self): + reference_price = get_order_reference_price({'type': 'Limit', 'price': '25000'}, 26000) + + self.assertEqual(reference_price, 25000) + + def test_get_order_reference_price_rejects_invalid_limit_price(self): + with self.assertRaises(ValueError): + get_order_reference_price({'type': 'Limit', 'price': '0'}, 26000) + + +if __name__ == '__main__': + unittest.main()