def handle_api_error(self, err: str, reason: str) -> pd.DataFrame: if self.debug: if self.die_on_api_error: raise SystemExit(err) else: Logger.debug(err) return pd.DataFrame() else: if self.die_on_api_error: raise SystemExit(f"{reason}: {self._api_url}") else: Logger.info(f"{reason}: {self._api_url}") return pd.DataFrame()
def getTakerFee(self, market: str = '') -> float: if len(market) != None: fees = self.getFees(market) else: fees = self.getFees() if len(fees) == 0 or 'taker_fee_rate' not in fees: Logger.error( f"error: 'taker_fee_rate' not in fees (using {DEFAULT_TAKER_FEE_RATE} as a fallback)" ) return DEFAULT_TAKER_FEE_RATE return float(fees['taker_fee_rate'].to_string(index=False).strip())
def getTime(self) -> datetime: """Retrieves the exchange time""" try: resp = self.authAPI("GET", "time") if "epoch" in resp: epoch = int(resp["epoch"]) return datetime.fromtimestamp(epoch) else: Logger.error(resp) return None except Exception as e: Logger.error(f"Error: {e}") return None
def _write_data(self, name: str = "") -> bool: file = self.filename if name == "" else name try: with open( os.path.join(self.app.telegramdatafolder, "telegram_data", file), "w", encoding="utf8", ) as outfile: json.dump(self.data, outfile, indent=4) return True except JSONDecodeError as err: Logger.critical(str(err)) return False
def getMakerFee(self, market: str='') -> float: if market == '': fees = self.getFees() else: fees = self.getFees(market) if len(fees) == 0 or 'maker_fee_rate' not in fees: Logger.error(f"error: 'maker_fee_rate' not in fees (using {DEFAULT_MAKER_FEE_RATE} as a fallback)") return DEFAULT_MAKER_FEE_RATE if market == '': return fees else: return float(fees['maker_fee_rate'].to_string(index=False).strip())
def minimumOrderBase(self, base, balancechk: bool = False): self.app.insufficientfunds = False if self.app.getExchange() == Exchange.BINANCE: df = self.api.getMarketInfoFilters(self.app.getMarket()) if len(df) > 0: base_min = float(df[df["filterType"] == "LOT_SIZE"][[ "minQty" ]].values[0][0]) base = float(base) elif self.app.getExchange() == Exchange.COINBASEPRO: product = self.api.authAPI("GET", f"products/{self.app.getMarket()}") if len(product) == 0: sys.tracebacklimit = 0 raise Exception(f"Market not found! ({self.app.getMarket()})") base = float(base) base_min = float(product["base_min_size"]) elif self.app.getExchange() == Exchange.KUCOIN: resp = self.api.authAPI("GET", "api/v1/symbols") product = resp[resp["symbol"] == self.app.getMarket()] if len(product) == 0: sys.tracebacklimit = 0 raise Exception(f"Market not found! ({self.app.getMarket()})") base = float(base) base_min = float(product["baseMinSize"]) # additional check for last order type if balancechk: if base > base_min: return True else: return elif base < base_min: if self.app.enableinsufficientfundslogging: self.app.insufficientfunds = True Logger.warning( f"Insufficient Base Funds! (Actual: {base}, Minimum: {base_min})" ) return sys.tracebacklimit = 0 raise Exception( f"Insufficient Base Funds! (Actual: {base}, Minimum: {base_min})" ) else: return
def __init__(self) -> None: for i in range(10): try: self.client = Client() break except Exception as e: if i == 9: raise SystemExit( "Can not create instance of AuthAPI client.") Logger.error('Exception: ' + str(e)) Logger.error( 'Error on creating instance of AuthAPI Client. Trying again... Attempt: ' + str(i)) sleep(0.1)
def saveCSV(self, filename: str='tradingdata.csv') -> None: """Saves the DataFrame to an uncompressed CSV.""" p = compile(r"^[\w\-. ]+$") if not p.match(filename): raise TypeError('Filename required.') if not isinstance(self.df, DataFrame): raise TypeError('Pandas DataFrame required.') try: self.df.to_csv(filename) except OSError: Logger.critical(f'Unable to save: {filename}')
def getTicker(self, market: str = DEFAULT_MARKET) -> tuple: """Retrieves the market ticker""" # validates the market is syntactically correct if not self._isMarketValid(market): raise TypeError("Kucoin market required.") resp = {} trycnt, maxretry = (0, 5) while trycnt <= maxretry: resp = self.authAPI( "GET", f"api/v1/market/orderbook/level1?symbol={market}") if "data" not in resp: # if not proper response, retry Logger.warning( f"Kucoin API Error for getTicket - 'data' not in response - retrying - attempt {trycnt}" ) time.sleep(15) trycnt += 1 elif "time" in resp["data"]: # if time returned, check format resptime = "" respprice = "" try: resptime = datetime.strptime( str( datetime.fromtimestamp( int(resp["data"]["time"]) / 1000)), "%Y-%m-%d %H:%M:%S.%f", ) respprice = float(resp["data"]["price"]) if resptime != "" and respprice != "": # if format is correct, return return ( datetime.strptime( str( datetime.fromtimestamp( int(resp["data"]["time"]) / 1000)), "%Y-%m-%d %H:%M:%S.%f", ).strftime("%Y-%m-%d %H:%M:%S"), respprice, ) except: Logger.warning( f"Kucoin API Error for Get Ticker - retrying - attempt {trycnt}" ) time.sleep(15) trycnt += 1 else: # time wasn't in response now = datetime.today().strftime("%Y-%m-%d %H:%M:%S") return (now, 0.0)
def marketSell(self, market: str = "", base_quantity: float = 0, test: bool = False) -> list: """Executes a market sell providing a crypto amount""" # validates the market is syntactically correct if not self._isMarketValid(market): raise ValueError("Binance market is invalid.") if not isinstance(base_quantity, int) and not isinstance( base_quantity, float): raise TypeError("The crypto amount is not numeric.") try: df_filters = self.getMarketInfoFilters(market) step_size = float(df_filters.loc[df_filters["filterType"] == "LOT_SIZE"]["stepSize"]) precision = int(round(-math.log(step_size, 10), 0)) # remove fees base_quantity = base_quantity - (base_quantity * self.getTradeFee(market)) # execute market sell stepper = 10.0**precision truncated = math.trunc(stepper * base_quantity) / stepper order = { "symbol": market, "side": "SELL", "type": "MARKET", "quantity": truncated, "recvWindow": self.recv_window, } Logger.debug(order) # POST /api/v3/order/test if test is True: resp = self.authAPI("POST", "/api/v3/order/test", order) else: resp = self.authAPI("POST", "/api/v3/order", order) return resp except Exception as err: ts = datetime.now().strftime("%d-%m-%Y %H:%M:%S") Logger.error(f"{ts} Binance marketSell {str(err)}") return []
def getMakerFee(self, market: str = "") -> float: """Retrieves the maker fee""" if len(market): fees = self.getFees(market) else: fees = self.getFees() if len(fees) == 0 or "maker_fee_rate" not in fees: Logger.error( f"error: 'maker_fee_rate' not in fees (using {DEFAULT_MAKER_FEE_RATE} as a fallback)" ) return DEFAULT_MAKER_FEE_RATE return float(fees["maker_fee_rate"].to_string(index=False).strip())
def handle_api_error(self, err: str, reason: str) -> dict: """Handler for API errors""" if self.debug: if self.die_on_api_error: raise SystemExit(err) else: Logger.error(err) return {} else: if self.die_on_api_error: raise SystemExit(f"{reason}: {self._api_url}") else: Logger.info(f"{reason}: {self._api_url}") return {}
def getTicker(self, market: str = DEFAULT_MARKET) -> tuple: """Retrieves the market ticker""" # validates the market is syntactically correct if not self._isMarketValid(market): raise TypeError("Kucoin market required.") resp = self.authAPI("GET", f"api/v1/market/orderbook/level1?symbol={market}") if "time" in resp["data"] and "price" in resp["data"]: # make sure the time format is correct, if not, pause and submit request again trycnt, maxretry = (1, 3) while trycnt <= maxretry: resptime = "" try: resptime = datetime.strptime( str( datetime.fromtimestamp( int(resp["data"]["time"]) / 1000)), "%Y-%m-%d %H:%M:%S.%f", ) except: Logger.warning( f"Kucoin API Error for Get Ticker: time format not correct - retrying - attempt {trycnt}" ) time.sleep(15) resp = self.authAPI( "GET", f"api/v1/market/orderbook/level1?symbol={market}") trycnt += 1 if resptime != "": break return ( datetime.strptime( str( datetime.fromtimestamp( int(resp["data"]["time"]) / 1000)), "%Y-%m-%d %H:%M:%S.%f", ).strftime("%Y-%m-%d %H:%M:%S"), float(resp["data"]["price"]), ) else: now = datetime.today().strftime("%Y-%m-%d %H:%M:%S") return (now, 0.0)
def authAPI(self, method: str, uri: str, payload: str = "") -> dict: """Initiates a REST API call""" if not isinstance(method, str): raise TypeError("Method is not a string.") if not method in ["GET", "POST"]: raise TypeError("Method not GET or POST.") if not isinstance(uri, str): raise TypeError("URI is not a string.") try: if method == "GET": resp = requests.get(self._api_url + uri) elif method == "POST": resp = requests.post(self._api_url + uri, json=payload) if resp.status_code != 200: resp_message = resp.json()["message"] message = f"{method} ({resp.status_code}) {self._api_url}{uri} - {resp_message}" if self.die_on_api_error: raise Exception(message) else: Logger.error(f"Error: {message}") return {} resp.raise_for_status() return resp.json() except requests.ConnectionError as err: Logger.error("requests.ConnectionError") # remove this later return self.handle_api_error(err, "ConnectionError") except requests.exceptions.HTTPError as err: Logger.error("requests.exceptions.HTTPError") # remove this later return self.handle_api_error(err, "HTTPError") except requests.Timeout as err: Logger.error("requests.Timeout") # remove this later return self.handle_api_error(err, "Timeout") except json.decoder.JSONDecodeError as err: Logger.error("json.decoder.JSONDecodeError") # remove this later return self.handle_api_error(err, "JSONDecodeError")
def _read_data(self, name: str = "") -> None: file = self.filename if name == "" else name try: with open( os.path.join(self.app.telegramdatafolder, "telegram_data", file), "r", encoding="utf8", ) as json_file: self.data = json.load(json_file) except (JSONDecodeError, Exception) as err: Logger.critical(str(err)) with open( os.path.join(self.app.telegramdatafolder, "telegram_data", file), "r", encoding="utf8", ) as json_file: self.data = json.load(json_file)
def marketBuy(self, market: str = "", quote_quantity: float = 0) -> pd.DataFrame: """Executes a market buy providing a funding amount""" # validates the market is syntactically correct if not self._isMarketValid(market): raise ValueError("Coinbase Pro market is invalid.") # validates quote_quantity is either an integer or float if not isinstance(quote_quantity, int) and not isinstance( quote_quantity, float): Logger.critical("Please report this to Michael Whittle: " + str(quote_quantity) + " " + str(type(quote_quantity))) raise TypeError("The funding amount is not numeric.") # funding amount needs to be greater than 10 if quote_quantity < MINIMUM_TRADE_AMOUNT: raise ValueError( f"Trade amount is too small (>= {MINIMUM_TRADE_AMOUNT}).") try: order = { "product_id": market, "type": "market", "side": "buy", "funds": self.marketQuoteIncrement(market, quote_quantity), } Logger.debug(order) # connect to authenticated coinbase pro api model = AuthAPI(self._api_key, self._api_secret, self._api_passphrase, self._api_url) # place order and return result return model.authAPI("POST", "orders", order) except: return pd.DataFrame()
def marketBuy(self, market: str = "", quote_quantity: float = 0) -> pd.DataFrame: """Executes a market buy providing a funding amount""" # validates the market is syntactically correct if not self._isMarketValid(market): raise ValueError("Kucoin market is invalid.") # validates quote_quantity is either an integer or float if not isinstance(quote_quantity, int) and not isinstance( quote_quantity, float): Logger.critical("Please report this to Michael Whittle: " + str(quote_quantity) + " " + str(type(quote_quantity))) raise TypeError("The funding amount is not numeric.") # funding amount needs to be greater than 10 if quote_quantity < MINIMUM_TRADE_AMOUNT: raise ValueError( f"Trade amount is too small (>= {MINIMUM_TRADE_AMOUNT}).") dt_obj = datetime.strptime(str(datetime.now()), "%Y-%m-%d %H:%M:%S.%f") millisec = dt_obj.timestamp() * 1000 order = { "clientOid": str(millisec), "symbol": market, "type": "market", "side": "buy", "funds": self.marketQuoteIncrement(market, quote_quantity), } # Logger.debug(order) # connect to authenticated Kucoin api model = AuthAPI(self._api_key, self._api_secret, self._api_passphrase, self._api_url) # place order and return result return model.authAPI("POST", "api/v1/orders", order)
def isSellSignal(self) -> bool: # required technical indicators or candle sticks for buy signal strategy required_indicators = ["ema12ltema26co", "macdltsignal"] for indicator in required_indicators: if indicator not in self._df_last: raise AttributeError(f"'{indicator}' not in Pandas dataframe") # criteria for a sell signal 1 if ( bool(self._df_last["ema12ltema26co"].values[0]) is True and ( bool(self._df_last["macdltsignal"].values[0]) is True or self.app.disableBuyMACD() ) and self.state.last_action not in ["", "SELL"] ): Logger.debug("*** Sell Signal ***") for indicator in required_indicators: Logger.debug(f"{indicator}: {self._df_last[indicator].values[0]}") Logger.debug(f"last_action: {self.state.last_action}") return True return False
def isSellSignal(self) -> bool: # required technical indicators or candle sticks for buy signal strategy required_indicators = [ 'ema12ltema26co', 'macdltsignal', 'macdltsignalco' ] for indicator in required_indicators: if indicator not in self._df_last: raise AttributeError(f"'{indicator}' not in Pandas dataframe") # if EMA, MACD are disabled, do not sell if self.app.disableBuyEMA() and self.app.disableBuyMACD(): return False # criteria for a sell signal 1 if (bool(self._df_last['ema12ltema26co'].values[0]) is True or self.app.disableBuyEMA()) \ and (bool(self._df_last['macdltsignalco'].values[0]) is True or self.app.disableBuyMACD()) \ and self.state.last_action not in ['', 'SELL']: Logger.debug('*** Sell Signal ***') for indicator in required_indicators: Logger.debug( f'{indicator}: {self._df_last[indicator].values[0]}') Logger.debug(f'last_action: {self.state.last_action}') return True return False
def checkTrailingBuy(self, app, state, price: float = 0.0): # If buy signal, save the price and check if it decreases before buying. trailing_buy_logtext = "" waitpcnttext = "" immediate_action = False if state.trailing_buy == 0: state.waiting_buy_price = price pricechange = 0 elif state.trailing_buy == 1 and state.waiting_buy_price > 0: pricechange = ((price - state.waiting_buy_price) / state.waiting_buy_price * 100) if price < state.waiting_buy_price: state.waiting_buy_price = price waitpcnttext += f"Price decreased - resetting wait price. " waitpcnttext += f"** {app.getMarket()} - " if pricechange < app.getTrailingBuyPcnt( ): # get pcnt from config, if not, use 0% state.action = "WAIT" state.trailing_buy = 1 if app.getTrailingBuyPcnt() > 0: trailing_buy_logtext = f" - Wait Chg: {_truncate(pricechange,2)}%/{app.getTrailingBuyPcnt()}%" waitpcnttext += f"Waiting to buy until {state.waiting_buy_price} increases {app.getTrailingBuyPcnt()}% - change {_truncate(pricechange,2)}%" else: trailing_buy_logtext = f" - Wait Chg: {_truncate(pricechange,2)}%" waitpcnttext += f"Waiting to buy until {state.waiting_buy_price} stops decreasing - change {_truncate(pricechange,2)}%" else: state.action = "BUY" state.trailing_buy = 1 if app.trailingImmediateBuy(): immediate_action = True trailing_buy_logtext = f" - Ready Chg: {_truncate(pricechange,2)}%/{app.getTrailingBuyPcnt()}%" waitpcnttext += f"Ready to buy. {state.waiting_buy_price} change of {_truncate(pricechange,2)}% is above setting of {app.getTrailingBuyPcnt()}%" if app.isVerbose() and (not app.isSimulation() or (app.isSimulation() and not app.simResultOnly())): Logger.info(waitpcnttext) return state.action, state.trailing_buy, trailing_buy_logtext, immediate_action
def _read_data(self, name: str = "") -> bool: file = self.filename if name == "" else name read_ok, try_count = False, 0 while not read_ok and try_count <= 5: try_count += 1 try: with open( os.path.join(self.app.telegramdatafolder, "telegram_data", file), "r", encoding="utf8", ) as json_file: self.data = json.load(json_file) read_ok = True except FileNotFoundError: Logger.warning("File Not Found: Recreating File..") self.create_bot_data() except JSONDecodeError: if len(self.data) > 0: Logger.warning("JSON Decode Error: Recreating File..") if name == "": self._write_data() else: Logger.warning("JSON Decode Error: Removing File..") if name == "": self.removeactivebot() return read_ok
def is6hEMA1226Bull(self, iso8601end: str = ''): try: if self.isSimulation() and isinstance(self.ema1226_6h_cache, pd.DataFrame): df_data = self.ema1226_6h_cache[(self.ema1226_6h_cache['date'] <= iso8601end)] elif self.exchange == 'coinbasepro': api = CBPublicAPI() df_data = api.getHistoricalData(self.market, 21600) self.ema1226_6h_cache = df_data elif self.exchange == 'binance': api = BPublicAPI() df_data = api.getHistoricalData(self.market, '6h') self.ema1226_6h_cache = df_data else: return False ta = TechnicalAnalysis(df_data) if 'ema12' not in df_data: ta.addEMA(12) if 'ema26' not in df_data: ta.addEMA(26) df_last = ta.getDataFrame().copy().iloc[-1, :] df_last['bull'] = df_last['ema12'] > df_last['ema26'] Logger.debug("---- EMA1226 6H Check----") if self.isSimulation(): Logger.debug("simdate: " + str(df_last['date'])) Logger.debug("ema12 6h: " + str(df_last['ema12'])) Logger.debug("ema26 6h: " + str(df_last['ema26'])) Logger.debug("bull 6h: " + str(df_last['ema12'] > df_last['ema26'])) return bool(df_last['bull']) except Exception: return False
def is1hSMA50200Bull(self, iso8601end: str = ''): try: if self.isSimulation() and isinstance(self.sma50200_1h_cache, pd.DataFrame): df_data = self.sma50200_1h_cache[( self.sma50200_1h_cache['date'] <= iso8601end)] elif self.exchange == 'coinbasepro': api = CBPublicAPI() df_data = api.getHistoricalData(self.market, 3600) self.sma50200_1h_cache = df_data elif self.exchange == 'binance': api = BPublicAPI() df_data = api.getHistoricalData(self.market, '1h') self.sma50200_1h_cache = df_data else: return False ta = TechnicalAnalysis(df_data) if 'sma50' not in df_data: ta.addSMA(50) if 'sma200' not in df_data: ta.addSMA(200) df_last = ta.getDataFrame().copy().iloc[-1, :] Logger.debug("---- SMA50200 1H Check----") if self.isSimulation(): Logger.debug("simdate: " + str(df_last['date'])) Logger.debug("sma50 1h: " + str(df_last['sma50'])) Logger.debug("sma200 1h: " + str(df_last['sma200'])) Logger.debug("bull 1h: " + str(df_last['sma50'] > df_last['sma200'])) df_last['bull'] = df_last['sma50'] > df_last['sma200'] return bool(df_last['bull']) except Exception: return False
def marketSell(self, market: str = '', base_quantity: float = 0) -> pd.DataFrame: if not self._isMarketValid(market): raise ValueError('Coinbase Pro market is invalid.') if not isinstance(base_quantity, int) and not isinstance( base_quantity, float): raise TypeError('The crypto amount is not numeric.') order = { 'product_id': market, 'type': 'market', 'side': 'sell', 'size': self.marketBaseIncrement(market, base_quantity) } Logger.debug(order) model = AuthAPI(self._api_key, self._api_secret, self._api_passphrase, self._api_url) return model.authAPI('POST', 'orders', order)
def authAPI(self, method: str, uri: str, payload: str = '') -> dict: if not isinstance(method, str): raise TypeError('Method is not a string.') if not method in ['GET', 'POST']: raise TypeError('Method not GET or POST.') if not isinstance(uri, str): raise TypeError('URI is not a string.') try: if method == 'GET': resp = requests.get(self._api_url + uri) elif method == 'POST': resp = requests.post(self._api_url + uri, json=payload) if resp.status_code != 200: resp_message = resp.json()['message'] message = f"{method} ({resp.status_code}) {self._api_url}{uri} - {resp_message}" if self.die_on_api_error: raise Exception(message) else: Logger.error(f"Error: {message}") return {} resp.raise_for_status() return resp.json() except requests.ConnectionError as err: return self.handle_api_error(err, "ConnectionError") except requests.exceptions.HTTPError as err: return self.handle_api_error(err, "HTTPError") except requests.Timeout as err: return self.handle_api_error(err, "Timeout") except json.decoder.JSONDecodeError as err: return self.handle_api_error(err, "JSONDecodeError")
def marketBuy(self, market: str = '', quote_quantity: float = 0) -> list: """Executes a market buy providing a funding amount""" # validates the market is syntactically correct if not self._isMarketValid(market): raise ValueError('Binance market is invalid.') # validates quote_quantity is either an integer or float if not isinstance(quote_quantity, int) and not isinstance( quote_quantity, float): raise TypeError('The funding amount is not numeric.') try: current_price = self.getTicker(market)[1] base_quantity = np.divide(quote_quantity, current_price) df_filters = self.getMarketInfoFilters(market) step_size = float(df_filters.loc[df_filters['filterType'] == 'LOT_SIZE']['stepSize']) precision = int(round(-math.log(step_size, 10), 0)) # remove fees base_quantity = base_quantity - (base_quantity * self.getTradeFee(market)) # execute market buy stepper = 10.0**precision truncated = math.trunc(stepper * base_quantity) / stepper Logger.info('Order quantity after rounding and fees: ' + str(truncated)) return self.client.order_market_buy(symbol=market, quantity=truncated) except Exception as err: ts = datetime.now().strftime("%d-%m-%Y %H:%M:%S") Logger.error(ts + ' Binance ' + ' marketBuy ' + str(err)) return []
def marketBuy(self, market: str = '', quote_quantity: float = 0) -> pd.DataFrame: """Executes a market buy providing a funding amount""" # validates the market is syntactically correct if not self._isMarketValid(market): raise ValueError('Coinbase Pro market is invalid.') # validates quote_quantity is either an integer or float if not isinstance(quote_quantity, int) and not isinstance( quote_quantity, float): Logger.critical('Please report this to Michael Whittle: ' + str(quote_quantity) + ' ' + str(type(quote_quantity))) raise TypeError('The funding amount is not numeric.') # funding amount needs to be greater than 10 if quote_quantity < MINIMUM_TRADE_AMOUNT: raise ValueError( f"Trade amount is too small (>= {MINIMUM_TRADE_AMOUNT}).") order = { 'product_id': market, 'type': 'market', 'side': 'buy', 'funds': self.marketQuoteIncrement(market, quote_quantity) } Logger.debug(order) # connect to authenticated coinbase pro api model = AuthAPI(self._api_key, self._api_secret, self._api_passphrase, self._api_url) # place order and return result return model.authAPI('POST', 'orders', order)
def printSupportResistanceLevel(self, price: float=0) -> None: if isinstance(price, int) or isinstance(price, float): df = self.getSupportResistanceLevels() if len(df) > 0: df_last = df.tail(1) if float(df_last[0]) < price: Logger.info(' Support level of ' + str(df_last[0]) + ' formed at ' + str(df_last.index[0])) elif float(df_last[0]) > price: Logger.info(' Resistance level of ' + str(df_last[0]) + ' formed at ' + str(df_last.index[0])) else: Logger.info(' Support/Resistance level of ' + str(df_last[0]) + ' formed at ' + str(df_last.index[0]))
def getTime(self) -> datetime: """Retrieves the exchange time""" try: resp = self.authAPI("GET", "time") if "epoch" in resp: epoch = int(resp["epoch"]) return datetime.fromtimestamp(epoch) else: Logger.error( "resp does not contain the epoch key for some reason!" ) # remove this later Logger.error(resp) return None except Exception as e: Logger.error(f"Error: {e}") return None
def isBuySignal(self, now: datetime = datetime.today().strftime( '%Y-%m-%d %H:%M:%S'), price: float = 0.0) -> bool: # required technical indicators or candle sticks for buy signal strategy required_indicators = [ 'ema12gtema26co', 'macdgtsignal', 'goldencross', 'obv_pc', 'eri_buy' ] for indicator in required_indicators: if indicator not in self._df_last: raise AttributeError(f"'{indicator}' not in Pandas dataframe") # buy signal exclusion (if disabled, do not buy within 3% of the dataframe close high) if self.state.last_action == 'SELL' and self.app.disableBuyNearHigh( ) is True and (price > (self._df['close'].max() * 0.97)): log_text = str(now) + ' | ' + self.app.getMarket( ) + ' | ' + self.app.printGranularity( ) + ' | Ignoring Buy Signal (price ' + str( price) + ' within 3% of high ' + str( self._df['close'].max()) + ')' Logger.warning(log_text) return False # criteria for a buy signal 1 if bool(self._df_last['ema12gtema26co'].values[0]) is True \ and (bool(self._df_last['macdgtsignal'].values[0]) is True or self.app.disableBuyMACD()) \ and (bool(self._df_last['goldencross'].values[0]) is True or self.app.disableBullOnly()) \ and (float(self._df_last['obv_pc'].values[0]) > -5 or self.app.disableBuyOBV()) \ and (bool(self._df_last['eri_buy'].values[0]) is True or self.app.disableBuyElderRay()) \ and self.state.last_action != 'BUY': # required for all strategies Logger.debug('*** Buy Signal ***') for indicator in required_indicators: Logger.debug( f'{indicator}: {self._df_last[indicator].values[0]}') Logger.debug(f'last_action: {self.state.last_action}') return True # criteria for buy signal 2 (optionally add additional buy singals) elif bool(self._df_last['ema12gtema26co'].values[0]) is True \ and bool(self._df_last['macdgtsignalco'].values[0]) is True \ and (bool(self._df_last['goldencross'].values[0]) is True or self.app.disableBullOnly()) \ and (float(self._df_last['obv_pc'].values[0]) > -5 or self.app.disableBuyOBV()) \ and (bool(self._df_last['eri_buy'].values[0]) is True or self.app.disableBuyElderRay()) \ and self.state.last_action != 'BUY': # required for all strategies Logger.debug('*** Buy Signal ***') for indicator in required_indicators: Logger.debug( f'{indicator}: {self._df_last[indicator].values[0]}') Logger.debug(f'last_action: {self.state.last_action}') return True return False