Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
36 changes: 22 additions & 14 deletions app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."
}
Comment on lines +173 to +190

else:
print("Invalid Exchange, Please Try Again!")
return {
"status": "error",
"message": "Invalid Exchange, Please Try Again!"
}

if __name__ == '__main__':
app.run(debug=False)


53 changes: 36 additions & 17 deletions binanceFutures.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
},
Expand All @@ -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',
},
Expand All @@ -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):
Expand All @@ -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()
Expand Down Expand Up @@ -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)
Expand All @@ -172,18 +193,17 @@ 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)


elif data['order_mode'] == 'Profit':
take_profit_percent = float(data['take_profit_percent']) / 100
current_price = exchange.fetch_ticker(data['symbol'])['last']

Comment on lines 205 to 207
if data['side'] == 'Buy':
take_profit_price = round(float(current_price) + (float(current_price) * take_profit_percent),
Expand All @@ -201,18 +221,17 @@ 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)


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)
Expand All @@ -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)
Expand Down
43 changes: 43 additions & 0 deletions order_sizing.py
Original file line number Diff line number Diff line change
@@ -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
52 changes: 52 additions & 0 deletions test_order_sizing.py
Original file line number Diff line number Diff line change
@@ -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()