1536 | # E: UpdateOrdersQueue: Unhandled exception. Cancelling: binance {"code":-1008,"msg":"Server is currently overloaded with other requests. Please try again in a few minutes."}
1537 | if( 'Too Many Requests' in a or 'too many request' in a
1538 | or 'service too busy' in a or 'system is busy' in a
1539 | or 'code":-1008' in a ):
1540 | #set a bigger delay and try again
1541 | order.delay += 1.0
1542 | print( " * Server too busy. Retrying.", type(e) )
1543 | continue
1544 |
1545 |
1546 | # [bitget/bitget] bitget {"code":"45110","msg":"less than the minimum amount 5 USDT","requestTime":1689481837614,"data":null}
1547 | # The deviation between your delegated price and the index price is greater than 20%, you can appropriately adjust your delegation price and try again
1548 | self.print( ' * E: UpdateOrdersQueue: Unhandled exception. Cancelling:', a, type(e) )
1549 | self.ordersQueue.remove( order )
1550 | continue # back to the orders loop
1551 |
1552 |
1553 | if( response.get('id') == None ):
1554 | self.print( " * E: Order denied:", response['info'], "Cancelling" )
1555 | self.ordersQueue.remove( order )
1556 | continue # back to the orders loop
1557 |
1558 | order.id = response.get('id')
1559 | status = response.get('status')
1560 | remaining = response.get('remaining')
1561 | if( remaining != None and remaining > 0 and (status == 'canceled' or status == 'closed') ):
1562 | print("r...", end = '')
1563 | self.ordersQueue.append( order_c( order.symbol, order.side, remaining, order.leverage, 0.5 ) )
1564 | self.ordersQueue.remove( order )
1565 | continue
1566 | if( (remaining == None or remaining == 0) and (response.get('status') == 'closed' or response.get('status') == 'filled') ):
1567 | self.print( " * Order successful:", order.symbol, order.side, order.quantity, str(order.leverage)+"x", "at price", response.get('price'), 'id', order.id )
1568 | self.ordersQueue.remove( order )
1569 | continue
1570 |
1571 | if verbose : print( timeNow(), " * Activating Order", order.symbol, order.side, order.quantity, str(order.leverage)+'x', 'id', order.id )
1572 | self.activeOrders.append( order )
1573 | self.ordersQueue.remove( order )
1574 |
1575 |
1576 | def proccessAlert( self, alert:dict ):
1577 |
1578 | self.print( ' ' )
1579 | self.print( " ALERT:", alert['alert'] )
1580 | self.print('----------------------------')
1581 |
1582 | # This is our first communication with the server, and (afaik) it will only fail when the server is not available.
1583 | # so we use it as a server availability check as well as for finding the available balance
1584 | try:
1585 | available = self.fetchAvailableBalance() * 0.985
1586 | except Exception as e:
1587 | a = e.args[0]
1588 | if( isinstance(e, ccxt.OnMaintenance) or isinstance(e, ccxt.NetworkError)
1589 | or isinstance(e, ccxt.RateLimitExceeded) or isinstance(e, ccxt.RequestTimeout)
1590 | or isinstance(e, ccxt.ExchangeNotAvailable) or 'not available' in a ):
1591 | # ccxt.base.errors.ExchangeError: Service is not available during funding fee settlement. Please try again later.
1592 | if( alert.get('timestamp') + ALERT_TIMEOUT < time.monotonic() ):
1593 | newAlert = copy.deepcopy( alert ) # the other alert will be deleted
1594 | if( isinstance(e, ccxt.RateLimitExceeded) ):
1595 | newAlert['delayTimestamp'] = time.monotonic() + 1
1596 | self.print( " * E: Rate limit exceeded. Retrying..." )
1597 | else:
1598 | newAlert['delayTimestamp'] = time.monotonic() + 30
1599 | self.print( " * E: Couldn't reach the server: Retrying in 30 seconds", e, type(e) )
1600 | self.latchedAlerts.append( newAlert )
1601 | else:
1602 | self.print( " * E: Couldn't reach the server: Cancelling" )
1603 | else:
1604 | self.print( " * E: Couldn't fetch balance: Cancelling", e, type(e) )
1605 | return
1606 |
1607 | #
1608 | # TEMP: convert to the old vars. I'll change it later (maybe)
1609 | #
1610 | symbol = alert['symbol']
1611 | command = alert['command']
1612 | quantity = alert['quantity']
1613 | leverage = alert['leverage'] if alert['leverage'] != 0 else self.markets[ symbol ]['local']['leverage']
1614 | isUSDT = alert['isUSDT']
1615 | isBaseCurrency = alert['isBaseCurrency']
1616 | isPercentage = alert['isPercentage']
1617 | lockBaseCurrency = alert['lockBaseCurrency']
1618 | priceLimit = alert['priceLimit']
1619 | customID = alert['customID']
1620 | usdtValue = None
1621 | isLimit = True if priceLimit > 0.0 else False
1622 |
1623 | if( verbose ):
1624 | print( "PROCESSALERT: isUSDT:", isUSDT, "isBaseCurrency:", isBaseCurrency )
1625 |
1626 | #time to put the order on the queue
1627 |
1628 | # No point in putting cancel orders in the queue. Just do it and leave.
1629 | if( command == 'cancel' ):
1630 | if( customID == 'all' ):
1631 | self.cancelAllOrders( symbol )
1632 | else:
1633 | self.cancelLimitOrder( symbol, customID )
1634 | return
1635 |
1636 | # bybit is too slow at updating positions after an order is made, so make sure they're updated
1637 | if( self.exchange.id == 'bybit' and (command == 'position' or command == 'close') ):
1638 | self.refreshPositions( False )
1639 |
1640 | minOrder = self.findMinimumAmountForSymbol(symbol)
1641 | leverage = self.verifyLeverageRange( symbol, leverage )
1642 |
1643 | # quantity is a percentage of the USDT balance
1644 | if( isPercentage and command != 'close' ):
1645 | quantity = min( max( float(quantity), -100.0 ), 100.0 )
1646 | balance = float( self.fetchBalance().get( 'total' ) )
1647 | quantity = round( balance * quantity * 0.01, 4 )
1648 | isUSDT = True
1649 |
1650 | if quantity is not None and abs(quantity) < FLOAT_ERROR:
1651 | quantity = 0.0
1652 |
1653 | # convert quantity to concracts if needed
1654 | if( (isUSDT or isBaseCurrency) and quantity != 0.0 ) :
1655 |
1656 | # when using base currency with bclock, and contractsize is 1 we don't have to do any conversion
1657 | if not ( isBaseCurrency and lockBaseCurrency and self.findContractSizeForSymbol(symbol) == 1 ):
1658 |
1659 | # We don't know for sure yet if it's a buy or a sell, so we average
1660 | oldQuantity = quantity
1661 | try:
1662 | price = self.fetchAveragePrice(symbol)
1663 |
1664 | except ccxt.ExchangeError as e:
1665 | self.print( " * E: proccessAlert->fetchAveragePrice:", e )
1666 | return
1667 | except ValueError as e:
1668 | self.print( " * E: proccessAlert->fetchAveragePrice", e, type(e) )
1669 | return
1670 |
1671 | coin_name = self.markets[symbol]['quote']
1672 |
1673 | if( isBaseCurrency ) :
1674 | if( lockBaseCurrency and leverage > 1 ):
1675 | quantity = quantity * price / leverage
1676 | else:
1677 | quantity *= price
1678 |
1679 | coin_name = self.markets[symbol]['base']
1680 |
1681 | usdtValue = quantity
1682 | quantity = self.contractsFromUSDT( symbol, quantity, price, leverage )
1683 | if verbose : print( " CONVERTING (x"+str(leverage)+")", oldQuantity, coin_name, '==>', quantity, "contracts" )
1684 |
1685 | if( abs(quantity) < minOrder ):
1686 | self.print( " * E: Order too small:", quantity, "Minimum required:", minOrder )
1687 | return
1688 |
1689 | # check for a existing position
1690 | pos = self.getPositionBySymbol( symbol )
1691 |
1692 | if( command == 'changeleverage' ):
1693 | if( pos == None ):
1694 | self.print( " * E: No position to change leverage" )
1695 | return
1696 | if( self.markets[ symbol ]['local']['leverage'] == leverage ):
1697 | self.print( " * Position already has leverage:", leverage )
1698 | return
1699 | self.ordersQueue.append( order_c( symbol, 'changeleverage', leverage = leverage ) )
1700 | return
1701 |
1702 |
1703 | if( command == 'close' or (command == 'position' and abs(quantity) < FLOAT_ERROR ) ):
1704 | if pos == None:
1705 | self.print( " * 'Close", symbol, "' No position found" )
1706 | return
1707 | positionContracts = pos.getKey('contracts')
1708 | positionSide = pos.getKey( 'side' )
1709 |
1710 | if( command == 'close' and isPercentage ):
1711 | quantity = min( abs(quantity), 100.0 )
1712 | positionContracts = positionContracts * quantity * 0.01
1713 |
1714 |
1715 | if( positionSide == 'long' ):
1716 | self.ordersQueue.append( order_c( symbol, 'sell', positionContracts, 0 ) )
1717 | else:
1718 | self.ordersQueue.append( order_c( symbol, 'buy', positionContracts, 0 ) )
1719 |
1720 | return
1721 |
1722 | # position orders are absolute. Convert them to buy/sell order
1723 | if( command == 'position' ):
1724 | if( pos == None or pos.getKey('contracts') == None ):
1725 | # it's just a straight up buy or sell
1726 | if( quantity < 0 ):
1727 | command = 'sell'
1728 | else:
1729 | command = 'buy'
1730 | quantity = abs(quantity)
1731 |
1732 | # FIXME: Buy/sell commands can change marginmode in Bitget and retain the position. Maybe this isn't needed.
1733 | elif( self.markets[symbol]['local']['marginMode'] != self.MARGIN_MODE and self.exchange.has['setMarginMode'] ):
1734 | # to change marginMode we need to close the old position first
1735 | if( pos.getKey('side') == 'long' ):
1736 | self.ordersQueue.append( order_c( symbol, 'sell', pos.getKey('contracts'), 0 ) )
1737 | else:
1738 | self.ordersQueue.append( order_c( symbol, 'buy', pos.getKey('contracts'), 0 ) )
1739 | # Then create the order for the new position
1740 | if( quantity < 0 ):
1741 | command = 'sell'
1742 | else:
1743 | command = 'buy'
1744 | quantity = abs(quantity)
1745 | else:
1746 | # we need to account for the old position
1747 | positionContracts = pos.getKey('contracts')
1748 | positionSide = pos.getKey( 'side' )
1749 | if( positionSide == 'short' ):
1750 | positionContracts = -positionContracts
1751 |
1752 | # !! We have to recalculate *from USDT* when the price is above the entry in a LONG and below the entry in a SHORT
1753 | if( usdtValue != None and positionSide == ("short" if quantity < 0.0 else "long") ):
1754 | extraMargin = 0
1755 | entryPrice = float(pos.getKey('entryPrice'))
1756 | initialMargin = (positionContracts * entryPrice)/float(self.markets[ symbol ]['local']['leverage'])
1757 |
1758 | if( initialMargin != -1 ):
1759 | #if we're going to change the leverage we need to manipulate the initial margen
1760 | if( leverage != self.markets[ symbol ]['local']['leverage'] ):
1761 | #if the new leverage is bigger the margin will be reduced
1762 | initialMargin = initialMargin * ( float(self.markets[ symbol ]['local']['leverage'] / float(leverage)) )
1763 |
1764 | if( positionSide == 'long' ):
1765 | extraMargin = usdtValue - initialMargin
1766 | quantity = positionContracts + self.contractsFromUSDT( symbol, extraMargin, price, leverage )
1767 |
1768 | elif( positionSide == 'short' ):
1769 | extraMargin = abs(usdtValue) - initialMargin
1770 | quantity = positionContracts - self.contractsFromUSDT( symbol, extraMargin, price, leverage )
1771 |
1772 |
1773 |
1774 | command = 'sell' if positionContracts > quantity else 'buy'
1775 | quantity = abs( quantity - positionContracts )
1776 | if( quantity < minOrder ):
1777 | # we don't need to buy nor sell, but do we need to change the leverage?
1778 | if( leverage != self.markets[ symbol ]['local']['leverage'] ):
1779 | self.ordersQueue.append( order_c( symbol, 'changeleverage', leverage = leverage ) )
1780 | else:
1781 | self.print( " * Order completed: Request matched current position" )
1782 | return
1783 | # if we are reducing the size and changing leverage we want to reduce size first, then modify the leverage
1784 | if( command == 'sell' and leverage != self.markets[ symbol ]['local']['leverage'] and self.markets[ symbol ]['local']['leverage'] != 0 ): # kucoin has 0 local leverage until an order is processed
1785 | alert = {
1786 | 'symbol': symbol,
1787 | 'command': 'changeleverage',
1788 | 'quantity': None,
1789 | 'leverage': leverage,
1790 | 'isUSDT': False,
1791 | 'isBaseCurrency': False,
1792 | 'isPercentage': False,
1793 | 'lockBaseCurrency': False,
1794 | 'priceLimit': 0.0,
1795 | 'customID': None,
1796 | 'alert': f"{symbol} changeleverage {leverage}",
1797 | 'timestamp':time.monotonic()
1798 | }
1799 | self.latchedAlerts.append( alert )
1800 | leverage = self.markets[ symbol ]['local']['leverage'] # reduce the position with current leverage
1801 | # fall through
1802 |
1803 |
1804 | if( command == 'buy' or command == 'sell'):
1805 |
1806 | # fetch available balance and price
1807 | price = self.fetchSellPrice(symbol) if( command == 'sell' ) else self.fetchBuyPrice(symbol)
1808 | canDoContracts = self.contractsFromUSDT( symbol, available, price, leverage )
1809 |
1810 | if( pos != None ):
1811 | positionContracts = pos.getKey('contracts')
1812 | positionSide = pos.getKey( 'side' )
1813 |
1814 | # reversing the position
1815 | if not isLimit and (( positionSide == 'long' and command == 'sell' ) or ( positionSide == 'short' and command == 'buy' )):
1816 |
1817 | # do we need to divide these in 2 orders?
1818 |
1819 | # bingx must make one order for close and a second one for the new position
1820 | if( self.exchange.id == 'bingx' ):
1821 | if( quantity > positionContracts ):
1822 | self.ordersQueue.append( order_c( symbol, command, positionContracts, 0 ) )
1823 | quantity -= positionContracts
1824 | self.ordersQueue.append( order_c( symbol, command, quantity, leverage ) )
1825 | return
1826 |
1827 | self.ordersQueue.append( order_c( symbol, command, quantity, leverage, reduceOnly=True ) )
1828 | return
1829 |
1830 | # FIXME: Bybit takes the fees on top of the order which makes it fail with insuficcient
1831 | # balance when we try to order all the balance at once, which creates complications
1832 | # when reducing a reveral order. This is a temporary way to make it work, but
1833 | # we should really calculate the fees
1834 | #
1835 | # FIXME: Temporarily using this path for OKX too
1836 | if( ( self.exchange.id == 'bybit' or self.exchange.id == 'okx' ) and quantity > positionContracts ):
1837 | self.ordersQueue.append( order_c( symbol, command, positionContracts, 0, reduceOnly = True ) )
1838 | quantity -= positionContracts
1839 | if( quantity > minOrder ):
1840 | self.ordersQueue.append( order_c( symbol, command, quantity, leverage ) )
1841 | return
1842 |
1843 | if( quantity >= canDoContracts + positionContracts ):
1844 | # we have to make sure each of the orders has the minimum order contracts
1845 | order1 = canDoContracts + positionContracts
1846 | order2 = quantity - (canDoContracts + positionContracts)
1847 | if( order2 < minOrder ):
1848 | diff = minOrder - order2
1849 | if( order1 > minOrder + diff ):
1850 | order1 -= diff
1851 |
1852 | # first order is the contracts in the position and the contracs we can afford with the liquidity
1853 | self.ordersQueue.append( order_c( symbol, command, order1, leverage ) )
1854 |
1855 | # second order is whatever we can afford with the former position contracts + the change
1856 | quantity -= order1
1857 | if( quantity >= minOrder ): #we are done (should never happen)
1858 | self.ordersQueue.append( order_c( symbol, command, quantity, leverage, 1.0 ) )
1859 |
1860 | return
1861 | # fall through
1862 |
1863 | if( quantity < minOrder ):
1864 | self.print( timeNow(), " * E: Order too small:", quantity, "Minimum required:", minOrder )
1865 | return
1866 |
1867 | order = order_c( symbol, command, quantity, leverage )
1868 | if( isLimit ):
1869 | order.type = 'limit'
1870 | order.customID = customID
1871 | order.price = priceLimit
1872 |
1873 | self.ordersQueue.append( order )
1874 | return
1875 |
1876 | self.print( " * E: Something went wrong. No order was placed")
1877 |
1878 |
1879 | accounts = []
1880 |
1881 |
1882 |
1883 |
1884 | def stringToValue( arg )->float:
1885 | try:
1886 | float(arg)
1887 | except ValueError:
1888 | value = None
1889 | else:
1890 | value = float(arg)
1891 | return value
1892 |
1893 |
1894 | def updateOrdersQueue():
1895 | for account in accounts:
1896 | numOrders = len(account.ordersQueue) + len(account.activeOrders)
1897 | account.updateOrdersQueue()
1898 |
1899 | # see if we have any alert pending to be proccessed
1900 | if( len(account.latchedAlerts) ):
1901 | positionsRefreshed = False
1902 | for alert in account.latchedAlerts:
1903 | if( alert.get('delayTimestamp') != None ):
1904 | alert.get('delayTimestamp') < time.monotonic()
1905 | continue
1906 |
1907 | busy = False
1908 | for order in account.activeOrders:
1909 | if( order.symbol == alert['symbol'] ):
1910 | busy = True
1911 | break
1912 | for order in account.ordersQueue:
1913 | if( order.symbol == alert['symbol'] ):
1914 | busy = True
1915 | break
1916 |
1917 | if( not busy ):
1918 | if( not positionsRefreshed ):
1919 | account.refreshPositions(False)
1920 | positionsRefreshed = True
1921 |
1922 | account.proccessAlert( alert )
1923 | account.latchedAlerts.remove( alert )
1924 |
1925 | # if we just cleared the orders queue refresh the positions info
1926 | if( numOrders > 0 and (len(account.ordersQueue) + len(account.activeOrders)) == 0 ):
1927 | account.refreshPositions(True)
1928 |
1929 |
1930 | def refreshPositions():
1931 | for account in accounts:
1932 | account.refreshPositions()
1933 |
1934 |
1935 | def generatePositionsString()->str:
1936 | msg = ''
1937 | for account in accounts:
1938 | account.refreshPositions()
1939 | numPositions = len(account.positionslist)
1940 | balanceString = ''
1941 | if SHOW_BALANCE:
1942 | try:
1943 | balance = account.fetchBalance()
1944 | except Exception as e:
1945 | balanceString = ''
1946 | else:
1947 | balanceString = " * Balance: {:.2f}[$]".format(balance['total'])
1948 | balanceString += " - Available {:.2f}[$]".format(balance['free'])
1949 |
1950 | msg += '---------------------\n'
1951 | msg += 'Refreshing positions '+account.accountName+': ' + str(numPositions) + ' positions found' + balanceString + '\n'
1952 | if( numPositions == 0 ):
1953 | continue
1954 |
1955 | for pos in account.positionslist:
1956 | msg += pos.generatePrintString() + '\n'
1957 |
1958 | return msg
1959 |
1960 | def parseAlert( data, account: account_c ):
1961 |
1962 | if( account == None ):
1963 | return { 'Error': " * E: parseAlert called without an account" }
1964 |
1965 | alert = {
1966 | 'symbol': None,
1967 | 'command': None,
1968 | 'quantity': None,
1969 | 'leverage': 0,
1970 | 'isUSDT': False,
1971 | 'isBaseCurrency': False,
1972 | 'isPercentage': False,
1973 | 'lockBaseCurrency': False,
1974 | 'priceLimit': 0.0,
1975 | 'customID': None,
1976 | 'alert': data,
1977 | 'timestamp':time.monotonic()
1978 | }
1979 |
1980 | limitToken = None
1981 | cancelToken = None
1982 |
1983 | # Informal plain text syntax
1984 | tokens = data.split()
1985 | for token in tokens:
1986 | if( account.findSymbolFromPairName(token) != None ): # GMXUSDTM, GMX/USDT:USDT and GMX/USDT are all acceptable formats
1987 | alert['symbol'] = account.findSymbolFromPairName(token)
1988 | elif ( token.lower() == account.accountName.lower() ):
1989 | pass
1990 | elif ( token[:1].lower() == "$" or token[-1:] == "$" ): # value in USDT
1991 | alert['isUSDT'] = True
1992 | arg = token.lower().strip().replace("$", "")
1993 | alert['quantity'] = stringToValue( arg )
1994 | elif ( token[:1].lower() == "@" or token[-1:] == "@" ): # value in contracts
1995 | arg = token.lower().strip().replace("@", "")
1996 | alert['quantity'] = stringToValue( arg )
1997 | elif ( token[:1].lower() == "%" or token[-1:] == "%" ): # value in percentage of balance
1998 | arg = token.lower().strip().replace("%", "")
1999 | alert['quantity'] = stringToValue( arg )
2000 | alert['isPercentage'] = True
2001 | elif ( token[:1] == "-" ): # this is a minus symbol! What a bitch (value in base currency)
2002 | alert['isBaseCurrency'] = True
2003 | alert['quantity'] = stringToValue( token )
2004 | elif ( stringToValue( token ) != None ):
2005 | alert['isBaseCurrency'] = True
2006 | arg = token
2007 | alert['quantity'] = stringToValue(arg)
2008 | elif token.lower() == 'force_usdt':
2009 | alert['isUSDT'] = True
2010 | elif token.lower() == 'force_percent':
2011 | alert['isPercentage'] = True
2012 | elif token.lower() == 'force_basecurrency':
2013 | alert['isBaseCurrency'] = True
2014 | elif token.lower() == 'lockbasecurrency' or token.lower() == "bclock":
2015 | alert['lockBaseCurrency'] = True
2016 | elif ( token[:1].lower() == "x" or token[-1:].lower() == "x"):
2017 | arg = token.lower().strip().replace("x", "")
2018 | leverage = stringToValue(arg)
2019 | alert['leverage'] = int(leverage) if leverage is not None else 0
2020 | elif token.lower() == 'long':
2021 | alert['command'] = 'buy'
2022 | print( "WARNING: 'long' and 'short' commands are deprecated and will be removed in the future. Please use 'buy' and 'sell' instead" )
2023 | elif token.lower() == 'short':
2024 | alert['command'] = 'sell'
2025 | print( "WARNING: 'long' and 'short' commands are deprecated and will be removed in the future. Please use 'buy' and 'sell' instead" )
2026 | elif token.lower() == "buy":
2027 | alert['command'] = 'buy'
2028 | elif token.lower() == "sell":
2029 | alert['command'] = 'sell'
2030 | elif token.lower() == 'close':
2031 | alert['command'] = 'close'
2032 | elif token.lower() == 'position' or token.lower() == 'pos':
2033 | alert['command'] = 'position'
2034 | elif token.lower() == 'changeleverage':
2035 | alert['command'] = 'changeleverage'
2036 | elif ( token[:5].lower() == "limit" ):
2037 | limitToken = token # we validate it at processing
2038 | elif ( token[:6].lower() == "cancel" ):
2039 | cancelToken = token # we validate it at processing
2040 | alert['command'] = 'cancel'
2041 | else:
2042 | print( "Unknown alert command:", token )
2043 |
2044 | if( alert['isPercentage'] ):
2045 | alert['isBaseCurrency'] = False
2046 | alert['isUSDT'] = False
2047 | if( alert['isUSDT'] ):
2048 | alert['isBaseCurrency'] = False
2049 |
2050 | # do some syntax validation
2051 | if( alert['symbol'] == None ):
2052 | return { 'Error': " * E: Couldn't find symbol" }
2053 |
2054 | if( alert['command'] == None ):
2055 | return { 'Error': " * E: Invalid Order: Missing command" }
2056 |
2057 | if( alert['command'] == 'buy' or alert['command'] == 'sell' or alert['command'] == 'position' ):
2058 | if( alert['quantity'] == None ):
2059 | return { 'Error': " * E: Invalid quantity value" }
2060 | if( alert['quantity'] < 0 and alert['command'] == 'buy' ):
2061 | return { 'Error': " * E: Invalid Order: Buy must have a positive amount" }
2062 | if( alert['quantity'] == 0 and alert['command'] != 'position' ):
2063 | return { 'Error':" * E: Invalid Order amount: 0" }
2064 | if( alert['command'] == 'sell' and alert['quantity'] < 0 ): # be flexible with sell having a negative amount
2065 | alert['quantity'] = abs(alert['quantity'])
2066 |
2067 | if( alert['command'] == "changeleverage" ):
2068 | alert['isBaseCurrency'] = False
2069 | alert['isUSDT'] = False
2070 | alert['isBaseCurrency'] = False
2071 | if( alert['quantity'] == None ):
2072 | alert['quantity'] = alert['leverage']
2073 | if( alert['quantity'] == 0 ):
2074 | return { 'Error': " * E: Couldn't find a leverage value for setleverage" }
2075 | if( alert['leverage'] == 0 ):
2076 | alert['leverage'] = int( alert['quantity'] )
2077 |
2078 | # parse de cancel and limit tokens
2079 | if( limitToken != None ):
2080 | if( alert['command'] != 'buy' and alert['command'] != 'sell' ):
2081 | return { 'Error': " * E: Limit orders can only be used with buy/sell commands" }
2082 |
2083 | v = limitToken.split(':')
2084 | if( len(v) != 3 ):
2085 | return { 'Error': " * E: Limit command must be formatted as 'limit:customID:price' " }
2086 | else:
2087 | alert['customID'] = v[1]
2088 | alert['priceLimit'] = stringToValue(v[2])
2089 | if( alert['priceLimit'] == None ):
2090 | return { 'Error': " * E: Limit command must be formatted as 'limit:customID:price' " }
2091 | if( alert['priceLimit'] <= 0 ):
2092 | return { 'Error': " * E: price limit must be bigger than 0" }
2093 |
2094 | if ( cancelToken != None ):
2095 | v = cancelToken.split(':')
2096 | if( len(v) != 2 ):
2097 | return { 'Error': " * E: Cancel command must be formatted as 'cancel:customID' " }
2098 | alert['customID'] = v[1]
2099 |
2100 | if( alert['customID'] != None ):
2101 | if( len(alert['customID']) < 2 or len(alert['customID']) > 30 ):
2102 | return { 'Error': " * E: customID must be longer than 2 characters and shorter than 30' " }
2103 | if( account.exchange.id == 'coinex' and not alert['customID'].isdigit() ):
2104 | return { 'Error': " * E: Coinex only accepts numeric customID' " }
2105 |
2106 | if verbose : print( alert )
2107 | return alert
2108 |
2109 |
2110 |
2111 | def Alert( data ):
2112 |
2113 | account = None
2114 |
2115 | # first lets find out if there's more than one commands inside the alert message
2116 | lines = data.split("\n")
2117 | for line in lines:
2118 | line = line.rstrip('\n')
2119 | if( len(line) == 0 ):
2120 | continue
2121 | if( line[:2] == '//' ): # if the line begins with // it's a comment and we skip it
2122 | continue
2123 | account = None
2124 | tokens = line.split()
2125 | for token in tokens:
2126 | for a in accounts:
2127 | if( token.lower() == a.accountName.lower() ):
2128 | account = a
2129 | break
2130 | if( account == None ):
2131 | print( timeNow(), ' * E: Account ID not found. ALERT:', line )
2132 | continue
2133 |
2134 | alert = parseAlert( line.replace('\n', ''), account )
2135 | if( alert.get('Error') != None ):
2136 | account.print( ' ' )
2137 | account.print( " ALERT:", line.replace('\n', '') )
2138 | account.print('----------------------------')
2139 | account.print( alert.get('Error') )
2140 | continue
2141 |
2142 | # check if the alert can be proccessed inmediately
2143 | busy = False
2144 | for o in account.activeOrders:
2145 | if( o.symbol == alert['symbol'] ):
2146 | busy = True
2147 | break
2148 | for o in account.ordersQueue:
2149 | if( o.symbol == alert['symbol'] ):
2150 | busy = True
2151 | break
2152 |
2153 | if( not busy ):
2154 | account.proccessAlert( alert )
2155 | continue
2156 |
2157 | # delay the alert proccessing
2158 | account.latchedAlerts.append( alert )
2159 |
2160 |
2161 |
2162 | ###################
2163 | #### Initialize ###
2164 | ###################
2165 |
2166 | print('----------------------------')
2167 |
2168 | #### Open accounts file ###
2169 |
2170 | try:
2171 | with open('accounts.json', 'r') as accounts_file:
2172 | accounts_data = json.load(accounts_file)
2173 | accounts_file.close()
2174 | except FileNotFoundError:
2175 | with open('accounts.json', 'x') as f:
2176 | f.write( '[\n\t{\n\t\t"ACCOUNT_ID":"your_account_name", \n\t\t"EXCHANGE":"exchange_name", \n\t\t"API_KEY":"your_api_key", \n\t\t"SECRET_KEY":"your_secret_key", \n\t\t"PASSWORD":"your_API_password", \n\t\t"MARGIN_MODE":"isolated"\n\t}\n]' )
2177 | f.close()
2178 | print( "File 'accounts.json' not found. Template created. Please fill your API Keys into the file and try again")
2179 | print( "Exiting." )
2180 | raise SystemExit()
2181 |
2182 | for ac in accounts_data:
2183 |
2184 | exchange = ac.get('EXCHANGE')
2185 | if( exchange == None ):
2186 | print( " * ERROR PARSING ACCOUNT INFORMATION: EXCHANGE" )
2187 | continue
2188 |
2189 | account_id = ac.get('ACCOUNT_ID')
2190 | if( account_id == None ):
2191 | print( " * ERROR PARSING ACCOUNT INFORMATION: ACCOUNT_ID" )
2192 | continue
2193 |
2194 | api_key = ac.get('API_KEY')
2195 | if( api_key == None ):
2196 | print( " * ERROR PARSING ACCOUNT INFORMATION: API_KEY" )
2197 | continue
2198 |
2199 | secret_key = ac.get('SECRET_KEY')
2200 | if( secret_key == None ):
2201 | print( " * ERROR PARSING ACCOUNT INFORMATION: SECRET_KEY" )
2202 | continue
2203 |
2204 | password = ac.get('PASSWORD')
2205 | if( password == None ):
2206 | password = ""
2207 | continue
2208 |
2209 | marginMode = ac.get('MARGIN_MODE')
2210 |
2211 | settleCoin = ac.get('SETTLE_COIN')
2212 |
2213 | print( timeNow(), " Initializing account: [", account_id, "] in [", exchange , ']')
2214 | try:
2215 | account = account_c( exchange, account_id, api_key, secret_key, password, marginMode, settleCoin )
2216 | except Exception as e:
2217 | print( 'Account creation failed:', e, type(e) )
2218 | print('------------------------------')
2219 | else:
2220 | accounts.append( account )
2221 |
2222 | if( len(accounts) == 0 ):
2223 | print( " * FATAL ERROR: No valid accounts found. Please edit 'accounts.json' and introduce your API keys" )
2224 | raise SystemExit()
2225 |
2226 |
2227 | ############################################
2228 |
2229 | # define the webhook server
2230 | app = Flask(__name__)
2231 | # silencing flask useless spam
2232 | log = logging.getLogger('werkzeug')
2233 | log.setLevel(logging.ERROR)
2234 | log.disabled = True
2235 |
2236 | if USE_PROXY == True:
2237 | # warn Flask that we are behind a Web proxy
2238 | app.wsgi_app = ProxyFix(
2239 | app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_prefix=1
2240 | )
2241 | PORT = PROXY_PORT
2242 |
2243 | @app.route('/whook', methods=['GET','POST'])
2244 | def webhook():
2245 |
2246 | if request.method == 'POST':
2247 | content_type = request.headers.get('Content-Type')
2248 | if content_type == 'application/json':
2249 | data = request.get_json()
2250 |
2251 | if data and 'update_id' in data: # Typical key in Telegram bot updates
2252 | # Extract message text and chat ID
2253 | if 'message' in data:
2254 | chat_id = data['message']['chat']['id']
2255 | message = data['message']['text']
2256 | # Log the received message
2257 | print( "Received message from chat_id", chat_id, ':', message )
2258 | return 'Telegram message processed', 200
2259 |
2260 | # we received a json of unknown source
2261 | return 'success', 200
2262 |
2263 | # Standard alert
2264 | data = request.get_data(as_text=True)
2265 | Alert(data)
2266 | return 'success', 200
2267 |
2268 | if request.method == 'GET':
2269 | # https://0.0.0.0/whook
2270 | response = request.args.get('response')
2271 | if( response == None ):
2272 | fontSize = 18
2273 | if fontSize > 0:
2274 | msg = f"""
2275 |
2276 |
2277 |
2278 | Positions
2279 |
2280 |
2281 | {generatePositionsString()}
2282 |
2283 |
2284 | """
2285 | return app.response_class( msg, mimetype='text/html; charset=utf-8' )
2286 | else:
2287 | msg = generatePositionsString()
2288 | return app.response_class( msg, mimetype='text/plain; charset=utf-8' )
2289 |
2290 | if response == 'whook':
2291 | return 'WHOOKITYWOOK'
2292 |
2293 | # https://0.0.0.0/whook?response=account
2294 | if response.lower() == 'allaccounts':
2295 | package = {"allaccounts": {}}
2296 | for acc in accounts:
2297 | acc.refreshPositions(False)
2298 | package["allaccounts"][acc.accountName] = { "positions": [pos.generateDictionary() for pos in acc.positionslist],
2299 | "balance": acc.fetchBalance().get('total') }
2300 | return jsonify(package)
2301 | else:
2302 | package = {"allaccounts": {}}
2303 | for acc in accounts:
2304 | if acc.accountName.lower() == response.lower():
2305 | acc.refreshPositions(False)
2306 | package["allaccounts"][acc.accountName] = { "positions": [pos.generateDictionary() for pos in acc.positionslist],
2307 | "balance": acc.fetchBalance().get('total') }
2308 | return jsonify(package)
2309 |
2310 | # temporarily disabled.
2311 | # Return the requested log file
2312 | # try:
2313 | # wmsg = open( f'{LOGS_DIRECTORY}/{response}.log', encoding="utf-8" )
2314 | # except FileNotFoundError:
2315 | # return 'Not found'
2316 | # else:
2317 | # text = wmsg.read()
2318 | # wmsg.close()
2319 | # return app.response_class(text, mimetype='text/plain; charset=utf-8')
2320 |
2321 | else:
2322 | abort(400)
2323 |
2324 | # start the positions fetching loop
2325 | timerFetchPositions = RepeatTimer( REFRESH_POSITIONS_FREQUENCY, refreshPositions )
2326 | timerFetchPositions.start()
2327 |
2328 | timerOrdersQueue = RepeatTimer( UPDATE_ORDERS_FREQUENCY, updateOrdersQueue )
2329 | timerOrdersQueue.start()
2330 |
2331 | # start the webhook server
2332 | if __name__ == '__main__':
2333 | print( " * Listening" )
2334 | app.run(host="0.0.0.0", port=PORT, debug=False)
2335 |
2336 |
2337 |
--------------------------------------------------------------------------------