# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
import datetime
import getpass
import json
import shutil
import math
import numbers
import os
import re
import time
import errno
from logging import DEBUG

import dill
import numpy

from urllib.parse import unquote
from PIL import Image
from selenium import webdriver
from selenium.common.exceptions import NoSuchElementException, StaleElementReferenceException, NoAlertPresentException, \
    TimeoutException, InvalidArgumentException, ElementClickInterceptedException, \
    WebDriverException, InvalidSessionIdException, SessionNotCreatedException
from selenium.webdriver import DesiredCapabilities, ActionChains
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.remote.webelement import WebElement
from selenium.webdriver.support import expected_conditions as ec
from selenium.webdriver.support.ui import WebDriverWait
from multiprocessing import Pool

from kairos import timing
from kairos import tools
from kairos.tools import format_number, wait_for_element_is_stale, print_dot
from fastnumbers import fast_real

TEST = False
processing_errors = []

triggered_signals = []
invalid = set()

EXECUTOR = 'http://192.168.0.140:4444/wd/hub'
FILENAME = 'webdriver.instance'
COUNTER_ALERTS = 0
TOTAL_ALERTS = 0
CURRENT_DIR = os.path.curdir
TEXT = 'text'
CHECKBOX = 'checkbox'
SELECTBOX = 'selectbox'
DATE = 'date'
TIME = 'time'
MAX_SCREENSHOTS_ON_ERROR = 0
WAIT_TIME_IMPLICIT_DEF = 30
PAGE_LOAD_TIMEOUT_DEF = 15
CHECK_IF_EXISTS_TIMEOUT_DEF = 15
DELAY_BREAK_MINI_DEF = 0.2
DELAY_BREAK_DEF = 0.5
DELAY_SUBMIT_ALERT_DEF = 3.5
DELAY_CLEAR_INACTIVE_ALERTS_DEF = 0
DELAY_CHANGE_SYMBOL_DEF = 0.2
DELAY_SCREENSHOT_DIALOG = 3
DELAY_SCREENSHOT = 1
DELAY_KEYSTROKE = 0.01
DELAY_WATCHLIST = 0.5
DELAY_TIMEFRAME = 0.5
DELAY_SCREENER_SEARCH = 2
DELAY_EXTRACT_SYMBOLS = 0.5
RUN_IN_BACKGROUND = False
MULTI_THREADING = False
ALERT_NUMBER = 0
SEARCH_FOR_WARNING = True
REFRESH_START = timing.time()
REFRESH_INTERVAL = 3600  # Refresh the browser each hour
ALREADY_LOGGED_IN = False

MODIFIER_KEY = Keys.LEFT_CONTROL
OS = tools.get_operating_system()
# if OS == 'macos':
#     MODIFIER_KEY = Keys.COMMAND

SELECT_ALL = MODIFIER_KEY + 'a'
CUT = MODIFIER_KEY + 'x'
PASTE = MODIFIER_KEY + 'v'
COPY = MODIFIER_KEY + 'c'

TV_UID = ''
TV_PWD = ''

NEGATIVE_COLOR = '#DD2E02'
WEBDRIVER_INSTANCE = 0

css_selectors = dict(
    # ALERTS
    username='span.tv-header__dropdown-text.tv-header__dropdown-text--username.js-username.tv-header__dropdown-text--ellipsis.apply-overflow-tooltip.common-tooltip-fixed',
    signin='body > div.tv-main > div.tv-header > div.tv-header__inner.tv-layout-width > div.tv-header__area.tv-header__area--right.tv-header__area--desktop > span.tv-header__dropdown-text > a',
    show_email_or_username='div.tv-signin-dialog__area.tv-signin-dialog__area--auth > div.js-pages-wrap > div > div.i-clearfix.active > div > span',
    input_username='#signin-form > div.tv-control-error > div.tv-control-material-input__wrap > input',
    input_password='#signin-form > div:nth-child(3) > div > div.tv-control-material-input__wrap > input',
    btn_login='#signin-form > div.tv-signin-dialog__footer.tv-signin-dialog__footer--login > div:nth-child(2) > button',
    btn_timeframe='#header-toolbar-intervals > div:last-child',
    options_timeframe='div[class^="dropdown-"] div[class^="item"]',
    input_watchlist_add_symbol='div.widgetbar-widget.widgetbar-widget-watchlist input',
    options_watchlist='div[data-name="menu-inner"] div[class^="item"]',
    input_symbol='#header-toolbar-symbol-search > div > input',
    asset='div[data-name="legend-series-item"] div[data-name="legend-source-title"]:nth-child(1)',
    btn_alert_menu='div.widgetbar-widget-alerts_manage > div > div > div:nth-child(2) > span',
    btn_dlg_clear_alerts_confirm='div.tv-dialog > div.tv-dialog__section--actions > div[data-name="yes"]',
    item_alerts='table.alert-list > tbody > tr.alert-item',
    btn_create_alert='#header-toolbar-alerts',
    btn_alert_cancel='div.tv-dialog__close.js-dialog__close',
    dlg_create_alert_first_row_first_item='fieldset > div:nth-child(1) > span > div:nth-child(1)',
    options_dlg_create_alert_first_row_first_item='fieldset > div:nth-child(1) > span > div:nth-child(1) span.tv-control-select__option-wrap',
    exists_dlg_create_alert_first_row_second_item='div.js-condition-first-operand-placeholder div.tv-alert-dialog__group-item--right > span > span',
    dlg_create_alert_first_row_second_item='div.js-condition-first-operand-placeholder div.tv-alert-dialog__group-item--right > span',
    options_dlg_create_alert_first_row_second_item='div.js-condition-first-operand-placeholder div.tv-alert-dialog__group-item--right span.tv-control-select__option-wrap',
    dlg_create_alert_second_row='fieldset > div:nth-child(2) > span',
    options_dlg_create_alert_second_row='fieldset > div:nth-child(2) > span span.tv-control-select__option-wrap',
    inputs_and_selects_create_alert_3rd_row_and_above='div.js-condition-second-operand-placeholder select, div.js-condition-second-operand-placeholder input',
    dlg_create_alert_3rd_row_group_item='div.js-condition-second-operand-placeholder div.tv-alert-dialog__group-item',
    options_dlg_create_alert_3rd_row_group_item='span.tv-control-select__dropdown.tv-dropdown-behavior__body.i-opened span.tv-control-select__option-wrap',
    selected_dlg_create_alert_3rd_row_group_item='span.tv-control-select__dropdown.tv-dropdown-behavior__body.i-opened > span > span > span:nth-child({0}) > span',
    checkbox_dlg_create_alert_frequency='div[data-title="{0}"]',
    # Notify on App
    clickable_dlg_create_alert_send_push='div.tv-alert-dialog__fieldset-value-item > label > span.tv-control-checkbox > input[name="send-push"] + span + span.tv-control-checkbox__ripple',
    # Show Popup
    clickable_dlg_create_alert_show_popup='div.tv-alert-dialog__fieldset-value-item > label > span.tv-control-checkbox > input[name="show-popup"] + span + span.tv-control-checkbox__ripple',
    # Send Email
    clickable_dlg_create_alert_send_email='div.tv-alert-dialog__fieldset-value-item > label > span.tv-control-checkbox > input[name="send-email"] + span + span.tv-control-checkbox__ripple',
    # Toggle more actions
    btn_toggle_more_actions='div.tv-alert-dialog__fieldset-wrapper-toggle.js-fieldset-wrapper-toggle',
    # Play Sound
    clickable_dlg_create_alert_play_sound='div.tv-alert-dialog__fieldset-value-item > label > span.tv-control-checkbox > input[name="play-sound"] + span + span.tv-control-checkbox__ripple',
    # Sound options
    dlg_create_alert_ringtone='div.js-sound-settings > div.tv-alert-dialog__group-item.tv-alert-dialog__group-item--left > span',
    options_dlg_create_alert_ringtone='div.js-sound-settings span.tv-control-select__dropdown.tv-dropdown-behavior__body.i-opened span.tv-control-select__option-wrap',
    dlg_create_alert_sound_duration='div.js-sound-settings > div.tv-alert-dialog__group-item.tv-alert-dialog__group-item--right > span',
    options_dlg_create_alert_sound_duration='div.js-sound-settings span.tv-control-select__dropdown.tv-dropdown-behavior__body.i-opened span.tv-control-select__option-wrap',
    # Send Email-to-SMS
    clickable_dlg_create_alert_send_email_to_sms='div.tv-alert-dialog__fieldset-value-item > label > span.tv-control-checkbox > input[name="send-sms"] + span + span.tv-control-checkbox__ripple',
    # Send SMS
    clickable_dlg_create_alert_send_sms='div.tv-alert-dialog__fieldset-value-item > label > span.tv-control-checkbox > input[name="send-true-sms"] + span + span.tv-control-checkbox__ripple',
    btn_dlg_create_alert_submit='div[data-name="submit"] > span.tv-button__loader',
    btn_create_alert_warning_continue_anyway='div[data-name^="warning"] button[name="ok-button"]',
    btn_alerts='div[data-name="alerts"]',
    btn_calendar='div[data-name="calendar"]',
    btn_watchlist='div[data-name="base"]',
    btn_watchlist_submenu='.widgetbar-widget-watchlist > div:nth-child(1) > div:nth-child(1)',
    div_watchlist_item='div[class^="wrap"] > div[class^="symbol"]',
    signout='body > div.tv-main.tv-screener__standalone-main-container > div.tv-header K> div.tv-header__inner.tv-layout-width > div.tv-header__area.tv-header__area--right.tv-header__area--desktop > span.tv-dropdown-behavior.tv-header__dropdown.tv-header__dropdown--user.i-opened > '
            'span.tv-dropdown-behavior__body.tv-header__dropdown-body.tv-header__dropdown-body--fixwidth.i-opened > span:nth-child(13) > a',
    checkbox_dlg_create_alert_open_ended='div.tv-alert-dialog__fieldset-value-item--open-ended input',
    clickable_dlg_create_alert_open_ended='div.tv-alert-dialog__fieldset-value-item--open-ended span.tv-control-checkbox__label',
    btn_dlg_screenshot='#header-toolbar-screenshot',
    dlg_screenshot_url='div[class^="copyForm"] > div > input',
    dlg_screenshot_close='div[class^="dialog"] > div > span[class^="close"]',
    # SCREENERS
    btn_filters='tv-screener-toolbar__button--filters',
    select_exchange='div.tv-screener-dialog__filter-field.js-filter-field.js-filter-field-exchange.tv-screener-dialog__filter-field--cat1.js-wrap.tv-screener-dialog__filter-field--active > '
                    'div.tv-screener-dialog__filter-field-content.tv-screener-dialog__filter-field-content--select.js-filter-field-_content > div > span',
    select_screener='div.tv-screener-toolbar__button.tv-screener-toolbar__button--with-options.tv-screener-toolbar__button--arrow-down.tv-screener-toolbar__button--with-state.apply-common-tooltip.common-tooltip-fixed.js-filter-sets.tv-dropdown-behavior__button',
    options_screeners='div.tv-screener-popup__item--presets > div.tv-dropdown-behavior__item',
    input_screener_search='div.tv-screener-table__search-query.js-search-query.tv-screener-table__search-query--without-description > input',
    # Strategy Tester
    tab_strategy_tester='#footer-chart-panel div[data-name=backtesting]',
    tab_strategy_tester_inactive='div[data-name="backtesting"][data-active="false"]',
    tab_strategy_tester_performance_summary='div.backtesting-select-wrapper > ul > li:nth-child(2)',
    btn_strategy_dialog='div.icon-button.js-backtesting-open-format-dialog',
    strategy_id='#bottom-area > div.bottom-widgetbar-content.backtesting > div.backtesting-head-wrapper > div:nth-child(1) > div > span',
    performance_overview_net_profit='div.report-data > div:nth-child(1) > strong',
    performance_overview_net_profit_percentage='div.report-data > div:nth-child(1) > p > span',
    performance_overview_total_closed_trades='div.report-data > div:nth-child(2) > strong',
    performance_overview_percent_profitable='div.report-data > div:nth-child(3) > strong',
    performance_overview_profit_factor='div.report-data > div:nth-child(4) > strong',
    performance_overview_max_drawdown='div.report-data > div:nth-child(5) > strong',
    performance_overview_max_drawdown_percentage='div.report-data > div:nth-child(5) > p > span',
    performance_overview_avg_trade='div.report-data > div:nth-child(6) > strong',
    performance_overview_avg_trade_percentage='div.report-data > div:nth-child(6) > p > span',
    performance_overview_avg_bars_in_trade='div.report-data > div:nth-child(7) > strong',
    performance_summary_net_profit='div.report-content.performance > div > table > tbody > tr:nth-child(1) > td:nth-child(2) > div:nth-child(1)',
    performance_summary_net_profit_percentage='div.report-content.performance > div > table > tbody > tr:nth-child(1) > td:nth-child(2) > div:nth-child(2) > span',
    performance_summary_total_closed_trades='div.report-content.performance > div > table > tbody > tr:nth-child(11) > td:nth-child(2)',
    performance_summary_percent_profitable='div.report-content.performance > div > table > tbody > tr:nth-child(15) > td:nth-child(2)',
    performance_summary_profit_factor='div.report-content.performance > div > table > tbody > tr:nth-child(7) > td:nth-child(2)',
    performance_summary_max_drawdown='div.report-content.performance > div > table > tbody > tr:nth-child(4) > td:nth-child(2) > div:nth-child(1)',
    performance_summary_max_drawdown_percentage='div.report-content.performance > div > table > tbody > tr:nth-child(4) > td:nth-child(2) > div:nth-child(2) > span',
    performance_summary_avg_trade='div.report-content.performance > div > table > tbody > tr:nth-child(16) > td:nth-child(2) > div:nth-child(1)',
    performance_summary_avg_trade_percentage='div.report-content.performance > div > table > tbody > tr:nth-child(16) > td:nth-child(2) > div:nth-child(2) > span',
    performance_summary_avg_bars_in_trade='div.report-content.performance > div > table > tbody > tr:nth-child(22) > td:nth-child(2)',
    # Indicator dialog
    indicator_dialog_tab_inputs='#overlap-manager-root div[class^="tab-"]:nth-child(1)',
    indicator_dialog_tab_properties='#overlap-manager-root div[class^="tab-"]:nth-child(2)',
    # indicator_dialog_tab_cells='#overlap-manager-root div[class^="content"] div[class^="cell-"] > div',
    indicator_dialog_tab_cells='#overlap-manager-root div[class^="content"] div[class^="cell-"]',
    indicator_dialog_tab_cell='#overlap-manager-root div[class^="content"] div[class^="cell-"]:nth-child({})',
    indicator_dialog_titles='#overlap-manager-root div[class^="content"] div[class*="first"] > div',
    indicator_dialog_checkbox_titles='#overlap-manager-root label[class^="checkbox"] span > span',
    indicator_dialog_checkbox='#overlap-manager-root label[class^="checkbox"] input:nth-child({})',
    indicator_dialog_value='#overlap-manager-root div[class^="content"] div[class*="last"] > div:nth-child({})',
    indicator_dialog_container='#overlap-manager-root div[class^="content"] div[class*="last"] div[class^="inputGroup"]',
    indicator_dialog_select_options='#overlap-manager-root div[class^="dropdown"] div[class^="item"]',
    btn_indicator_dialog_ok='#overlap-manager-root button[name="submit"]',
    active_chart_asset='div.chart-container.active div[class^="titleWrapper"] > div[data-name="legend-source-title"]:nth-child(1)',
    active_chart_interval='div.chart-container.active div[class^="titleWrapper"] > div[data-name="legend-source-title"]:nth-child(2)',
    # Indicator values
    span_indicator_loading='div[data-name^="legend-source-item"] > div[class^="valuesWrapper"] > span[class^="loader"]',
    # User Menu
    btn_user_menu="span.tv-dropdown-behavior.tv-header__dropdown.tv-header__dropdown--user",
    btn_logout="a[href='#signout']",
)

class_selectors = dict(
    form_create_alert='js-alert-form',
    rows_screener_result='tv-screener-table__result-row',
)

name_selectors = dict(
    checkbox_dlg_create_alert_show_popup='show-popup',
    checkbox_dlg_create_alert_play_sound='play-sound',
    checkbox_dlg_create_alert_send_email='send-email',
    checkbox_dlg_create_alert_email_to_sms='send-sms',
    # checkbox_dlg_create_alert_send_sms='send-true-sms',  # option removed by TradingView
    checkbox_dlg_create_alert_send_push='send-push'
)

tv_start = timing.time()
config = tools.get_config()
mode = 'a'  # append
if config.getboolean('logging', 'clear_on_start_up'):
    mode = 'w'  # overwrite
log = tools.create_log(mode)
log.setLevel(20)
# WARNING: debug level will log all HTTP requests
if config.has_option('logging', 'level'):
    log.setLevel(config.getint('logging', 'level'))

path_to_chromedriver = r"" + config.get('webdriver', 'path')
if os.path.exists(path_to_chromedriver):
    path_to_chromedriver = path_to_chromedriver.replace('.exe', '')
else:
    log.error("File {} does not exist".format(path_to_chromedriver))
    log.exception(FileNotFoundError)
    exit(0)

screenshot_dir = ''
if config.has_option('logging', 'screenshot_path'):
    screenshot_dir = config.get('logging', 'screenshot_path')
    if screenshot_dir != '':
        screenshot_dir = os.path.join(CURRENT_DIR, screenshot_dir)
    if not os.path.exists(screenshot_dir):
        # noinspection PyBroadException
        try:
            os.mkdir(screenshot_dir)
        except Exception as screenshot_error:
            log.info('No screenshot directory specified or unable to create it.')
            screenshot_dir = ''

if config.has_option('logging', 'max_screenshots_on_error'):
    MAX_SCREENSHOTS_ON_ERROR = config.getint('logging', 'max_screenshots_on_error')

WAIT_TIME_IMPLICIT = config.getfloat('webdriver', 'wait_time_implicit')
PAGE_LOAD_TIMEOUT = config.getfloat('webdriver', 'page_load_timeout')
CHECK_IF_EXISTS_TIMEOUT = config.getfloat('webdriver', 'check_if_exists_timeout')
DELAY_BREAK_MINI = config.getfloat('delays', 'break_mini')
DELAY_BREAK = config.getfloat('delays', 'break')
DELAY_SUBMIT_ALERT = config.getfloat('delays', 'submit_alert')
DELAY_CHANGE_SYMBOL = config.getfloat('delays', 'change_symbol')
DELAY_CLEAR_INACTIVE_ALERTS = config.getfloat('delays', 'clear_inactive_alerts')
if config.has_option('delays', 'screenshot_dialog'):
    DELAY_SCREENSHOT_DIALOG = config.getfloat('delays', 'screenshot_dialog')
if config.has_option('delays', 'screenshot'):
    DELAY_SCREENSHOT = config.getfloat('delays', 'screenshot')
if config.has_option('delays', 'keystroke'):
    DELAY_KEYSTROKE = 0.05
EXACT_CONDITIONS = config.getboolean('tradingview', 'exact_conditions')

RESOLUTION = '1920,1080'
if config.has_option('webdriver', 'resolution'):
    RESOLUTION = config.get('webdriver', 'resolution').strip(' ')


def close_all_popups(browser):
    for h in browser.window_handles[1:]:
        browser.switch_to.window(h)
        close_alerts(browser)
        browser.close()
    browser.switch_to.window(browser.window_handles[0])


def close_alerts(browser):
    try:
        alert = browser.switch_to.alert
        alert.accept()
    except NoAlertPresentException as e:
        log.debug(e)
    except Exception as e:
        log.exception(e)


def refresh(browser):
    log.debug('refreshing browser')
    browser.refresh()
    # Switching to Alert
    close_alerts(browser)
    # Close the watchlist menu if it is open
    if find_element(browser, css_selectors['btn_watchlist'], By.CSS_SELECTOR, False, False, 0.5):
        wait_and_click(browser, css_selectors['btn_watchlist'])


def element_exists(browser, locator, delay=CHECK_IF_EXISTS_TIMEOUT, locator_strategy = By.CSS_SELECTOR):
    result = False
    try:
        element = find_element(browser, locator, locator_strategy, delay)
        result = type(element) is WebElement
    except NoSuchElementException:
        log.debug('No such element. SELECTOR=' + locator)
        # print the session_id and url in case the element is not found
        # noinspection PyProtectedMember
        log.debug("In case you want to reuse session, the session_id and _url for current browser session are: {},{}".format(browser.session_id, browser.command_executor._url))
    except TimeoutException:
        log.debug('No such element. SELECTOR=' + locator)
    except Exception as element_exists_error:
        log.error(element_exists_error)
        log.debug("Check your locator: {}".format(locator))
        # noinspection PyProtectedMember
        log.debug("In case you want to reuse session, the session_id and _url for current browser session are: {},{}".format(browser.session_id, browser.command_executor._url))
    finally:
        log.debug("{} ({})".format(str(result), locator))
        return result


def wait_and_click(browser, locator, delay=CHECK_IF_EXISTS_TIMEOUT, locator_strategy=By.CSS_SELECTOR):
    return WebDriverWait(browser, delay).until(
        ec.element_to_be_clickable((locator_strategy, locator))).click()


def wait_and_click_by_xpath(browser, xpath, delay=CHECK_IF_EXISTS_TIMEOUT):
    WebDriverWait(browser, delay).until(
        ec.element_to_be_clickable((By.XPATH, xpath))).click()


def wait_and_click_by_text(browser, tag, search_text, css_class='', delay=CHECK_IF_EXISTS_TIMEOUT, position=0, postfix=''):
    if type(css_class) is str and len(css_class) > 0:
        xpath = '//{0}[contains(text(), "{1}") and @class="{2}"]{3}'.format(tag, search_text, css_class, postfix)
    else:
        xpath = '//{0}[contains(text(), "{1}")]{2}'.format(tag, search_text, postfix)
    if position == 0:
        WebDriverWait(browser, delay).until(
            ec.element_to_be_clickable((By.XPATH, xpath))).click()
    else:
        find_elements(browser, xpath, By.XPATH)[position].click()


def wait_and_get(browser, css, delay=CHECK_IF_EXISTS_TIMEOUT):
    element = WebDriverWait(browser, delay).until(
        ec.element_to_be_clickable((By.CSS_SELECTOR, css)))
    return element


def wait_and_visible(browser, css, delay=CHECK_IF_EXISTS_TIMEOUT):
    element = WebDriverWait(browser, delay).until(
        ec.visibility_of_element_located((By.CSS_SELECTOR, css)))
    return element


def find_element(browser, locator, locator_strategy=By.CSS_SELECTOR, except_on_timeout=True, visible=False, delay=CHECK_IF_EXISTS_TIMEOUT):
    if except_on_timeout:
        if visible:
            element = WebDriverWait(browser, delay).until(
                ec.visibility_of_element_located((locator_strategy, locator)))
        else:
            element = WebDriverWait(browser, delay).until(
                ec.presence_of_element_located((locator_strategy, locator)))
        return element
    else:
        try:
            if visible:
                element = WebDriverWait(browser, delay).until(
                    ec.visibility_of_element_located((locator_strategy, locator)))
            else:
                element = WebDriverWait(browser, delay).until(
                    ec.presence_of_element_located((locator_strategy, locator)))
            return element
        except TimeoutException as e:
            log.debug(e)
            log.debug("Check your {} locator: {}".format(locator_strategy, locator))
            # print the session_id and url in case the element is not found
            if browser is webdriver.Remote:
                # noinspection PyProtectedMember
                log.debug("In case you want to reuse session, the session_id and _url for current browser session are: {},{}".format(browser.session_id, browser.command_executor._url))


def find_elements(browser, locator, locator_strategy=By.CSS_SELECTOR, except_on_timeout=True, visible=False, delay=CHECK_IF_EXISTS_TIMEOUT):
    if except_on_timeout:
        if visible:
            elements = WebDriverWait(browser, delay).until(
                ec.visibility_of_all_elements_located((locator_strategy, locator)))
        else:
            elements = WebDriverWait(browser, delay).until(
                ec.presence_of_all_elements_located((locator_strategy, locator)))
        return elements
    else:
        try:
            if visible:
                elements = WebDriverWait(browser, delay).until(
                    ec.visibility_of_all_elements_located((locator_strategy, locator)))
            else:
                elements = WebDriverWait(browser, delay).until(
                    ec.presence_of_all_elements_located((locator_strategy, locator)))
            return elements
        except TimeoutException as e:
            log.debug(e)
            log.debug("Check your {} locator: {}".format(locator_strategy, locator))
            # print the session_id and url in case the element is not found
            # noinspection PyProtectedMember
            log.debug("In case you want to reuse session, the session_id and _url for current browser session are: {},{}".format(browser.session_id, browser.command_executor._url))
            return None


def hover(browser, element, click=False, delay=DELAY_BREAK_MINI):
    action = ActionChains(browser)
    action.move_to_element(element)
    if click:
        time.sleep(delay)
        action.click(element)
    action.perform()


def close_cookies_message(browser):
    xpath = '//strong[contains(text(), "cookies")]/following-sibling::div/button'
    try:
        wait_and_click_by_xpath(browser, xpath, 2)
        log.info("Cookie banner found")
    except TimeoutException as e:
        log.debug(e)
        log.info("Cookie banner not found")


def set_timeframe(browser, timeframe):
    log.info('Setting timeframe to ' + timeframe)
    wait_and_click(browser, css_selectors['btn_timeframe'])
    css = css_selectors['options_timeframe']
    el_options = find_elements(browser, css)
    index = 0
    found = False
    while not found and index < len(el_options):
        if el_options[index].text == timeframe:
            el_options[index].click()
            found = True
        index += 1
    if not found:
        log.warning('Unable to set timeframe to ' + timeframe)
        raise ValueError

    if found:
        # TODO replace 'element.send_keys" with
        #  action = ActionChains(browser)
        #  action.send_keys(Keys.TAB)
        #  action.perform()
        html = find_element(browser, 'html', By.TAG_NAME)
        html.send_keys(MODIFIER_KEY + 's')
        time.sleep(DELAY_BREAK)

    return found


def get_interval(timeframe):
    """
    Get TV's short timeframe notation
    :param timeframe: String.
    :return: interval: Short timeframe notation if found, empty string otherwise.
    """
    match = re.search(r"(\d+)\s(\w\w\w)", timeframe)
    interval = ""
    if match is None:
        log.warning("Cannot find match for timeframe '{}' with regex (\\d+)\\s(\\w\\w\\w). [0]".format(timeframe))
    else:
        try:
            interval = match.group(1)
            unit = match.group(2)
            if unit == 'day':
                interval += 'D'
            elif unit == 'wee':
                interval += 'W'
            elif unit == 'mon':
                interval += 'M'
            elif unit == 'hou':
                interval += 'H'
            elif unit == 'min':
                interval += ''
        except Exception as interval_exception:
            log.warning("Cannot find match for timeframe '{}' with regex (\\d+)\\s(\\w\\w\\w). [1]".format(timeframe))
            log.exception(interval_exception)
    return interval


def set_delays(chart):
    global WAIT_TIME_IMPLICIT
    global PAGE_LOAD_TIMEOUT
    global CHECK_IF_EXISTS_TIMEOUT
    global DELAY_BREAK_MINI
    global DELAY_BREAK
    global DELAY_SUBMIT_ALERT
    global DELAY_CLEAR_INACTIVE_ALERTS
    global DELAY_CHANGE_SYMBOL
    global DELAY_KEYSTROKE

    # set delays as defined within the chart with a fallback to the config file
    if 'wait_time_implicit' in chart and isinstance(chart['wait_time_implicit'], numbers.Real):
        WAIT_TIME_IMPLICIT = chart['wait_time_implicit']
    elif config.has_option('webdriver', 'wait_time_implicit'):
        WAIT_TIME_IMPLICIT = config.getfloat('webdriver', 'wait_time_implicit')

    if 'page_load_timeout' in chart and isinstance(chart['page_load_timeout'], numbers.Real):
        PAGE_LOAD_TIMEOUT = chart['page_load_timeout']
    elif config.has_option('webdriver', 'page_load_timeout'):
        PAGE_LOAD_TIMEOUT = config.getfloat('webdriver', 'page_load_timeout')

    if 'check_if_exists_timeout' in chart and isinstance(chart['check_if_exists_timeout'], numbers.Real):
        CHECK_IF_EXISTS_TIMEOUT = chart['check_if_exists_timeout']
    elif config.has_option('webdriver', 'check_if_exists_timeout'):
        CHECK_IF_EXISTS_TIMEOUT = config.getfloat('webdriver', 'check_if_exists_timeout')

    if 'delays' in chart and isinstance(chart['delays'], dict):
        delays = chart['delays']

        if 'change_symbol' in delays and isinstance(delays['change_symbol'], numbers.Real):
            DELAY_CHANGE_SYMBOL = delays['change_symbol']
        elif config.has_option('delays', 'change_symbol'):
            DELAY_CHANGE_SYMBOL = config.getfloat('delays', 'change_symbol')
        if 'submit_alert' in delays and isinstance(delays['submit_alert'], numbers.Real):
            DELAY_SUBMIT_ALERT = delays['submit_alert']
        elif config.has_option('delays', 'submit_alert'):
            DELAY_SUBMIT_ALERT = config.getfloat('delays', 'submit_alert')
        if 'break' in delays and isinstance(delays['break'], numbers.Real):
            DELAY_BREAK = delays['break']
        elif config.has_option('delays', 'break'):
            DELAY_BREAK = config.getfloat('delays', 'break')
        if 'break_mini' in delays and isinstance(delays['break_mini'], numbers.Real):
            DELAY_BREAK_MINI = delays['break_mini']
        elif config.has_option('delays', 'break_mini'):
            DELAY_BREAK_MINI = config.getfloat('delays', 'break_mini')
        if 'clear_inactive_alerts' in delays and isinstance(delays['clear_inactive_alerts'], numbers.Real):
            DELAY_CLEAR_INACTIVE_ALERTS = delays['clear_inactive_alerts']
        elif config.has_option('delays', 'clear_inactive_alerts'):
            DELAY_CLEAR_INACTIVE_ALERTS = config.getfloat('delays', 'clear_inactive_alerts')
        if 'keystroke' in delays and isinstance(delays['keystroke'], numbers.Real):
            DELAY_KEYSTROKE = delays['keystroke']
        elif config.has_option('delays', 'keystroke'):
            DELAY_KEYSTROKE = config.getfloat('delays', 'keystroke')


def wait_until_indicators_are_loaded(browser):
    indicator_loading = True
    log.info("indicators loading")
    while indicator_loading:
        indicator_loading = False
        # get css selector that has the loading animation
        elem_loading = find_elements(browser, css_selectors['span_indicator_loading'])
        # check if any of the elements is loaded
        for elem in elem_loading:
            indicator_loading = elem.is_displayed()
            if indicator_loading:
                break
    log.info("indicators loaded")


def is_indicator_loaded(browser, chart_index, pane_index, indicator_index, name=""):
    # get css selector that has the loading animation for the indicator
    elem_loading = find_elements(find_elements(
        find_elements(find_elements(browser, 'chart-container', By.CLASS_NAME)[chart_index], 'pane',
                      By.CLASS_NAME)[pane_index], 'div[data-name="legend-source-item"]', By.CSS_SELECTOR)[
                      indicator_index], 'div[class^="valuesWrapper"] > span[class^="loader"]', By.CSS_SELECTOR)
    # check if any of the elements is loaded
    indicator_loaded = True
    if len(elem_loading) == 0:
        if name != "":
            name = "{} at index ".format(name)
        log.warn("unable to find 'loading' elements of indicator {}{} on pane {} on chart {}".format(name, indicator_index, pane_index, chart_index))
    for elem in elem_loading:
        if elem.is_displayed():
            indicator_loaded = False
            break
    return indicator_loaded or len(elem_loading) == 0


def get_indicator_values(browser, indicator, symbol, previous_result, retry_number=0):
    result = []
    chart_index = -1
    pane_index = -1
    indicator_index = -1

    if 'chart_index' in indicator and str(indicator['chart_index']).isdigit():
        chart_index = indicator['chart_index']
    if 'pane_index' in indicator and str(indicator['pane_index']).isdigit():
        pane_index = indicator['pane_index']
    if 'indicator_index' in indicator and str(indicator['indicator_index']).isdigit():
        indicator_index = indicator['indicator_index']

    css = 'div.chart-container.active tr:nth-child({}) div[data-name="legend-source-item"] div[data-name="legend-source-title"]:nth-child(1)'.format((pane_index + 1) * 2 - 1)
    studies = find_elements(browser, css)
    if indicator_index < 0:
        try:
            for i, study in enumerate(studies):
                study_name = str(study.text)
                log.debug('Found {}'.format(study_name))
                if study_name.startswith(indicator['name']):
                    indicator_index = i
                    break
                try:
                    if str(study_name).lower().index('loading'):
                        time.sleep(0.1)
                        return retry_get_indicator_values(browser, indicator, symbol, previous_result, retry_number)
                    if str(study_name).lower().index('compiling'):
                        time.sleep(0.1)
                        return retry_get_indicator_values(browser, indicator, symbol, previous_result, retry_number)
                    if str(study_name).lower().index('error'):
                        time.sleep(0.1)
                        return retry_get_indicator_values(browser, indicator, symbol, previous_result, retry_number)
                except ValueError:
                    pass
        except StaleElementReferenceException:
            log.debug('StaleElementReferenceException in studies')
            return retry_get_indicator_values(browser, indicator, symbol, previous_result, retry_number)
        except TimeoutException:
            log.warning('timeout in finding studies')
            # return False which will force a browser refresh
            result = False
        except Exception as e:
            log.exception(e)
            return retry_get_indicator_values(browser, indicator, symbol, previous_result, retry_number)
        # use css
    try:
        if 0 <= indicator_index < len(studies):
            css = '#header-toolbar-symbol-search'
            element = find_element(browser, css)
            action = ActionChains(browser)
            action.move_to_element_with_offset(element, 5, 5)
            action.perform()

            indicator_name = ""
            if indicator['name']:
                indicator_name = indicator['name']
            log.debug("indicator {}loading".format(indicator_name + " "))
            loaded = False
            tries = 0
            while not loaded and tries < 200:
                tries += 1
                loaded = is_indicator_loaded(browser, chart_index, pane_index, indicator_index, indicator_name)
            # time.sleep(0.2)
            log.debug("indicator {}loaded (tries: {})".format(indicator_name + " ", tries))
            elem_values = find_elements(find_elements(find_elements(find_elements(browser, 'chart-container', By.CLASS_NAME)[chart_index], 'pane', By.CLASS_NAME)[pane_index], 'div[data-name="legend-source-item"]', By.CSS_SELECTOR)[indicator_index], 'div[class^="valuesAdditionalWrapper"] > div > div', By.CSS_SELECTOR)
            for e in elem_values:
                result.append(e.text)
    except StaleElementReferenceException:
        log.debug('StaleElementReferenceException in values')
        return retry_get_indicator_values(browser, indicator, symbol, previous_result, retry_number)
    except TimeoutException:
        log.warning('timeout in getting values', )
        # return False which will force a browser refresh
        result = False
    except Exception as e:
        log.exception(e)
        return retry_get_indicator_values(browser, indicator, symbol, previous_result, retry_number)

    # Check if we at least have a value, if not then the chart isn't loaded yet
    only_na_values = True
    for value in result:
        if value != 'n/a':
            only_na_values = False
            break

    # Check if there is a result and if the result differs from the previous result, otherwise we might have accidentally copied the values from the previous chart
    if not result or (isinstance(result, list) and len(result) == 0) or only_na_values or result == previous_result:
        # if only_na_values:
        #     time.sleep(0.1)
        return retry_get_indicator_values(browser, indicator, symbol, previous_result, retry_number)

    return result


def retry_get_indicator_values(browser, indicator, symbol, previous_result, retry_number=0):
    max_retries = config.getint('tradingview', 'create_alert_max_retries') * 10
    if config.has_option('tradingview', 'indicator_values_max_retries'):
        max_retries = config.getint('tradingview', 'indicator_values_max_retries')
    if retry_number < max_retries:
        return get_indicator_values(browser, indicator, symbol, previous_result, retry_number + 1)


def is_indicator_triggered(indicator, values):
    # log.info(values)
    result = False
    try:
        if 'trigger' in indicator:
            comparison = '='
            lhs = ''
            rhs = ''

            if 'type' in indicator['trigger']:
                comparison = indicator['trigger']['type']
            if 'left-hand-side' in indicator['trigger']:
                if 'index' in indicator['trigger']['left-hand-side'] and str(indicator['trigger']['left-hand-side']['index']).isdigit():
                    ignore = []
                    if 'ignore' in indicator['trigger']['left-hand-side'] and isinstance(indicator['trigger']['left-hand-side']['ignore'], list):
                        ignore = indicator['trigger']['left-hand-side']['ignore']
                    index = int(indicator['trigger']['left-hand-side']['index'])
                    try:
                        if index < len(values) and not (values[index] in ignore):
                            lhs = values[index]
                    except IndexError:
                        log.exception('YAML value trigger -> left-hand-side -> index is out of range. Index is {} but must be between 0 and {}'.format(str(index), str(len(values)-1)))
                if lhs == '' and indicator['trigger']['left-hand-side']['value'] != '':
                    lhs = indicator['trigger']['left-hand-side']['value']
            if 'right-hand-side' in indicator['trigger']:
                if 'index' in indicator['trigger']['right-hand-side'] and str(indicator['trigger']['right-hand-side']['index']).isdigit():
                    ignore = []
                    if 'ignore' in indicator['trigger']['right-hand-side'] and isinstance(indicator['trigger']['right-hand-side']['ignore'], list):
                        ignore = indicator['trigger']['right-hand-side']['ignore']
                    index = int(indicator['trigger']['right-hand-side']['index'])
                    try:
                        if index < len(values) and not (values[index] in ignore):
                            rhs = values[index]
                    except IndexError:
                        log.exception('YAML value trigger -> right-hand-side -> index is out of range. Index is {} but must be between 0 and {}'.format(str(index), str(len(values)-1)))
                if rhs == '' and indicator['trigger']['right-hand-side']['value'] != '':
                    rhs = indicator['trigger']['right-hand-side']['value']

            if not (lhs is None or lhs == '' or rhs is None or rhs == ''):
                try:
                    lhs = float(lhs)
                    rhs = float(rhs)
                except Exception as e:
                    log.debug(e)
                    lhs = str(lhs)
                    rhs = str(rhs)

                try:
                    if comparison == '=':
                        result = lhs == rhs
                    elif comparison == '!=':
                        result = lhs != rhs
                    elif comparison == '>=':
                        result = lhs >= rhs
                    elif comparison == '>':
                        result = lhs > rhs
                    elif comparison == '<=':
                        result = lhs <= rhs
                    elif comparison == '<':
                        result = lhs < rhs
                except Exception as e:
                    log.exception(e)
            else:
                if lhs == '':
                    lhs = 'undefined'
                if rhs == '':
                    rhs = 'undefined'
            log.debug('({} {} {}) returned {}'.format(str(lhs), comparison, str(rhs), str(result)))

        else:
            log.debug('No trigger information found, returning True')
            result = True
    except Exception as e:
        log.exception(e)
    return result


def save_strategy_results(data, save_as):
    filename = "{}_{}.json".format(save_as, datetime.datetime.today().strftime('%Y%m%d_%H%M'))
    if not os.path.exists('output'):
        os.mkdir('output')
    filename = os.path.join('output', filename)
    with open(filename, 'w+') as file:
        file.write(data)


def open_chart(browser, chart, save_as, counter_alerts, total_alerts):
    """
    :param browser:
    :param chart:
    :param save_as:
    :param counter_alerts:
    :param total_alerts:
    :return:

    TODO:   remember original setting of opened chart, and place them back when finished
    """
    global SEARCH_FOR_WARNING
    SEARCH_FOR_WARNING = True
    try:
        # load the chart
        close_all_popups(browser)
        log.info("opening chart " + chart['url'])

        # set wait times defined in chart
        set_delays(chart)
        log.info("WAIT_TIME_IMPLICIT = " + str(WAIT_TIME_IMPLICIT))
        log.info("PAGE_LOAD_TIMEOUT = " + str(PAGE_LOAD_TIMEOUT))
        log.info("CHECK_IF_EXISTS_TIMEOUT = " + str(CHECK_IF_EXISTS_TIMEOUT))
        log.info("DELAY_BREAK_MINI = " + str(DELAY_BREAK_MINI))
        log.info("DELAY_BREAK = " + str(DELAY_BREAK))
        log.info("DELAY_SUBMIT_ALERT = " + str(DELAY_SUBMIT_ALERT))
        log.info("DELAY_CHANGE_SYMBOL = " + str(DELAY_CHANGE_SYMBOL))
        log.info("DELAY_CLEAR_INACTIVE_ALERTS = " + str(DELAY_CLEAR_INACTIVE_ALERTS))
        log.info("DELAY_KEYSTROKE = " + str(DELAY_KEYSTROKE))

        url = unquote(chart['url'])
        browser.execute_script("window.open('" + url + "');")
        for handle in browser.window_handles[1:]:
            browser.switch_to.window(handle)

        wait_and_click(browser, css_selectors['btn_calendar'], 30)
        wait_and_click(browser, css_selectors['btn_watchlist'])
        time.sleep(DELAY_WATCHLIST)

        # get the symbols for each watchlist
        dict_watchlist = dict()
        for i, watchlist in enumerate(chart['watchlists']):
            watchlist = chart['watchlists'][i]
            # open list of watchlists element
            log.debug("collecting symbols from watchlist {}".format(watchlist))

            # check if watchlist is already opened
            try:
                xpath = '//div[@data-role="button"]/span/span[contains(text(), "{}")][1]'.format(watchlist)
                watchlist_opened = element_exists(browser, xpath, 0.5, By.XPATH)
            except TimeoutException:
                watchlist_opened = False

            # open watchlist
            if not watchlist_opened:
                wait_and_click(browser, css_selectors['input_symbol'])
                wait_and_click(browser, css_selectors['btn_watchlist_submenu'])
                time.sleep(DELAY_BREAK)
                try:
                    xpath = '//span[contains(text(), "{}")][last()]'.format(watchlist)
                    WebDriverWait(browser, CHECK_IF_EXISTS_TIMEOUT).until(ec.element_to_be_clickable((By.XPATH, xpath))).click()
                    wait_and_click_by_xpath(browser, xpath)
                    # html = find_element(browser, 'html')
                    # html.send_keys(Keys.ESCAPE)
                    watchlist_opened = True
                except Exception as e:
                    log.debug(e)

            if watchlist_opened:
                # wait until the list is loaded
                time.sleep(DELAY_EXTRACT_SYMBOLS)
                # extract symbols from watchlist
                symbols = []
                try:
                    # scroll down
                    last_symbol = "unknown"
                    previous_last_symbol = ""
                    while previous_last_symbol != last_symbol:
                        previous_last_symbol = last_symbol
                        run_again = True
                        while run_again:
                            run_again = False  # run only once by default
                            try:
                                dict_symbols = find_elements(browser, css_selectors['div_watchlist_item'], By.CSS_SELECTOR)
                                last_element = dict_symbols[len(dict_symbols)-1]
                                last_symbol = last_element.get_attribute('data-symbol-full')
                                for symbol in dict_symbols:
                                    symbols.append(symbol.get_attribute('data-symbol-full'))
                                    if len(symbols) >= config.getint('tradingview', 'max_symbols_per_watchlist'):
                                        break
                                ActionChains(browser).move_to_element(last_element).perform()
                            except StaleElementReferenceException:
                                run_again = True  # run again if we find StaleElementReferenceExceptions
                    symbols = list(dict.fromkeys(symbols))
                    log.info("{}: {} markets found".format(watchlist, len(symbols)))
                except Exception as e:
                    log.exception(e)
                    snapshot(browser)
                dict_watchlist[chart['watchlists'][i]] = symbols

        # close the watchlist menu to save some loading time
        wait_and_click(browser, css_selectors['btn_watchlist'])

        if 'strategies' in chart:
            date = datetime.datetime.strptime(time.strftime('%Y-%m-%dT%H:%M:%S%z', time.localtime()), '%Y-%m-%dT%H:%M:%S%z')
            btn_strategy_inactive = find_element(browser, css_selectors['tab_strategy_tester_inactive'], By.CSS_SELECTOR, False, True)
            if btn_strategy_inactive:
                btn_strategy_inactive.click()
                btn_performance_tab = find_element(browser, css_selectors['tab_strategy_tester_performance_summary'], By.CSS_SELECTOR, False, True)
                if btn_performance_tab:
                    btn_performance_tab.click()

            summaries = dict()
            summaries['chart'] = chart['url']
            summaries['datetime'] = date.strftime('%Y-%m-%d %H:%M:%S %z')

            # Sort if the user defined one for all strategies. This overrides sorting on a per strategy basis.
            sort = dict()
            for strategy in chart['strategies']:
                if 'sort' in strategy:
                    sort = strategy['sort']
                    log.info(sort)
                    continue
                log.info("running strategy {}".format(strategy['name']))
                if not strategy['name'] in summaries:
                    summaries[strategy['name']] = dict()
                    summaries[strategy['name']]['id'] = "unknown"
                    strategy_element = find_element(browser, css_selectors['strategy_id'])
                    if strategy_element:
                        summaries[strategy['name']]['id'] = strategy_element.text
                    default_chart_inputs, default_chart_properties = get_strategy_default_values(browser)
                    log.info("default_inputs: {}".format(default_chart_inputs))
                    log.info("default_properties: {}".format(default_chart_properties))
                    summaries[strategy['name']]['default_inputs'] = default_chart_inputs
                    summaries[strategy['name']]['default_properties'] = default_chart_properties
                else:
                    # ensure fall back to default inputs and properties
                    refresh(browser)

                # generate input/property sets
                atomic_inputs = []
                atomic_properties = []
                if 'inputs' in strategy:
                    inputs = get_config_values(strategy['inputs'])
                    generate_atomic_values(inputs, atomic_inputs)
                if 'properties' in strategy:
                    properties = get_config_values(strategy['properties'])
                    generate_atomic_values(properties, atomic_properties)
                log.info("{} tests will be run for each watchlist".format(max(1, len(atomic_inputs)) * max(1, len(atomic_properties))))

                sort_by = False
                if 'sort_by' in strategy:
                    sort_by = strategy['sort_by']
                reverse = False
                if 'sort_asc' in strategy:
                    reverse = not strategy['sort_asc']

                # test the strategy and sort the results
                for watchlist in chart['watchlists']:
                    symbols = dict_watchlist[watchlist]
                    test_results = back_test(browser, strategy, symbols, atomic_inputs, atomic_properties)
                    # sort if the user defined one for the strategy
                    if sort_by:
                        test_results = back_test_sort_watchlist(test_results, sort_by, reverse)

                    if watchlist in summaries[strategy['name']]:
                        summaries[strategy['name']][watchlist] += test_results
                    else:
                        summaries[strategy['name']][watchlist] = test_results

            # Sort if the user defined one for all strategies. This overrides sorting on a per strategy basis.
            if sort:
                log.info('sort')
                if 'sort_by' in sort:
                    sort_by = sort['sort_by']
                    reverse = False
                    if 'sort_asc' in sort:
                        reverse = not sort['sort_asc']
                    back_test_sort(summaries, sort_by, reverse)

            # Save the results
            filename = save_as
            match = re.search(r"([\w\-_]*)", save_as)
            if match:
                filename = match.group(1)
            elif save_as == "":
                filename = "run"
            save_strategy_results(json.dumps(summaries, indent=4), filename)

        if 'alerts' in chart or 'signals' in chart:
            # time.sleep(5)
            # set the time frame
            for timeframe in chart['timeframes']:
                set_timeframe(browser, timeframe)
                time.sleep(DELAY_TIMEFRAME)

                # iterate over each symbol per watchlist
                for watchlist in chart['watchlists']:
                    log.info("opening watchlist " + watchlist)
                    try:
                        number_of_windows = 2
                        symbols = dict_watchlist[watchlist]

                        # log.info(__name__)
                        if MULTI_THREADING:
                            batch_size = math.ceil(len(symbols) / number_of_windows)
                            batches = list(tools.chunks(symbols, batch_size))

                            browsers = dict()

                            if __name__ == 'tv.tv':
                                pool = Pool(number_of_windows)  # use all available cores, otherwise specify the number you want as an argument
                                for k, batch in enumerate(batches):
                                    batch = batches[k]
                                    if k == 0:
                                        browsers[k] = browser
                                    else:
                                        browsers[k] = get_browser_instance()
                                    result = pool.apply_async(process_symbols, args=(browser, chart, batch, timeframe, counter_alerts, total_alerts,))
                                    log.info(result)
                                    # [counter_alerts, total_alerts]
                                    # pool.apply_async(process_symbols, args=(browser, chart, batch, timeframe))
                                pool.close()
                                pool.join()
                        else:
                            [counter_alerts, total_alerts] = process_symbols(browser, chart, symbols, timeframe, counter_alerts, total_alerts)
                        # pickle.dump(browser, 'webdriver.instance')
                    except KeyError:
                        log.error(watchlist + " doesn't exist")
                        break
    except Exception as exc:
        log.exception(exc)
        snapshot(browser)
    return [counter_alerts, total_alerts]


def process_symbols(browser, chart, symbols, timeframe, counter_alerts, total_alerts):
    log.info(timeframe)
    # open each symbol within the watchlist
    for k, symbol in enumerate(symbols):
        use_space = False
        if k > 0:
            use_space = False
        [counter_alerts, total_alerts] = process_symbol(browser, chart, symbols[k], timeframe, counter_alerts, total_alerts, use_space)
    return [counter_alerts, total_alerts]


def process_symbol(browser, chart, symbol, timeframe, counter_alerts, total_alerts, use_space=False, retry_number=0):
    log.info(symbol)
    # change symbol
    try:
        # Try to browse through the watchlist using space instead of setting the symbol value
        if use_space:
            action = ActionChains(browser)
            action.send_keys(Keys.SPACE)
            action.perform()
        else:
            # might be useful for multi threading set the symbol by going to different url like this:
            # https://www.tradingview.com/chart/?symbol=BINANCE%3AAGIBTC
            input_symbol = find_element(browser, css_selectors['input_symbol'])
            set_value(browser, input_symbol, symbol)
            input_symbol.send_keys(Keys.ENTER)

        #########################################################################################################
        # Wait until the chart is loaded.
        # NOTE: indicators are also checked if they are loaded before reading their values
        #########################################################################################################
        wait_until_chart_is_loaded(browser)

    except Exception as err:
        if symbol not in processing_errors:
            processing_errors.append(symbol)
        log.debug('unable to change to symbol')
        log.exception(err)
        snapshot(browser)

    # if element_exists(browser, '#overlap-manager-root > div > div._tv-dialog-nonmodal.ui-draggable'):
    #     action = ActionChains(browser)
    #     action.send_keys(Keys.ESCAPE)
    #     action.perform()

    # check for errors /
    previous_values = []
    last_indicator_name = ""
    try:
        if 'signals' in chart:
            for signal in chart['signals']:

                triggered = []
                indicators = signal['indicators']
                timestamp = time.time()

                data = dict()
                data['timestamp'] = timestamp
                data['date_utc'] = datetime.datetime.utcfromtimestamp(timestamp).strftime("%a, %d %b %Y %H:%M:%S") + ' +0000'
                data['date'] = datetime.datetime.fromtimestamp(timestamp).strftime("%a, %d %b %Y %H:%M:%S %z") + tools.get_timezone()
                data['timeframe'] = timeframe
                data['symbol'] = symbol
                [data['exchange'], data['ticker']] = str(symbol).split(':')
                data['name'] = signal['name']

                interval = get_interval(timeframe)
                data['interval'] = interval
                url = browser.current_url + '?symbol=' + symbol
                multi_time_frame_layout = False
                try:
                    multi_time_frame_layout = signal['multi_time_frame_layout']
                except KeyError:
                    if log.level == 10:
                        log.warn('charts: multi_time_frame_layout not set in yaml, defaulting to multi_time_frame_layout = no')
                if type(interval) is str and len(interval) > 0 and not multi_time_frame_layout:
                    url += '&interval=' + str(interval)
                data['url'] = url

                signal_triggered = True
                for m, indicator in enumerate(indicators):
                    indicator = indicators[m]

                    # We don't have to extract the values if the previous indicator is the same as the current one, e.g. when finding both short and long signals
                    if indicator['name'] == last_indicator_name and previous_values:
                        values = previous_values
                    else:
                        values = get_indicator_values(browser, indicator, symbol, previous_values)
                        log.debug(values)
                        previous_values = values
                    last_indicator_name = indicator['name']

                    # if we can't find a value, process this symbol again until we hit max retries which at that point we assume that the symbol doesn't exist or only hav 'n/a' values is correct
                    if (not values) and retry_number < config.getint('tradingview', 'create_alert_max_retries'):
                        return retry_process_symbol(browser, chart, symbol, timeframe, counter_alerts, total_alerts, retry_number)
                    signal['indicators'][m]['values'] = values
                    indicator_triggered = is_indicator_triggered(indicator, values)
                    if not indicator_triggered:
                        signal_triggered = False
                        break
                    signal['indicators'][m]['triggered'] = indicator_triggered
                    triggered.append(indicator_triggered)
                    if 'data' in indicator:
                        for item in indicator['data']:
                            for _key in item:
                                if not (_key in data):
                                    if isinstance(item[_key], list):
                                        indices = item[_key]
                                        data[_key] = []
                                        for index in indices:
                                            if index < len(values):
                                                data[_key].append(values[index])
                                    else:
                                        index = item[_key]
                                        if index < len(values):
                                            data[_key] = values[index]
                    # use tab to put focus on the next layout
                    # TODO replace 'element.send_keys" with
                    #  action = ActionChains(browser)
                    #  action.send_keys(Keys.TAB)
                    #  action.perform()
                    html = find_element(browser, 'html', By.TAG_NAME)
                    html.send_keys(Keys.TAB)
                    previous_values = values

                if signal_triggered:
                    signal['triggered'] = signal_triggered
                    screenshots = dict()
                    filenames = dict()
                    screenshots_url = []
                    asset = ''
                    for m in range(5):
                        try:
                            el_asset_name = find_element(browser, css_selectors['asset'])
                            asset = el_asset_name.text
                            break
                        except StaleElementReferenceException:
                            log.warning('Unable to retrieve asset name... trying again')
                            pass
                    try:
                        for m, screenshot_chart in enumerate(signal['include_screenshots_of_charts']):
                            screenshot_chart = unquote(signal['include_screenshots_of_charts'][m])
                            [screenshot_url, filename] = take_screenshot(browser, symbol, interval)
                            if screenshot_url != '':
                                screenshots[screenshot_chart] = screenshot_url
                                screenshots_url.append(screenshot_url)
                                if m == 0:
                                    data['screenshot'] = screenshot_url
                            if filename != '':
                                filenames[screenshot_chart] = filename
                    except ValueError as value_error:
                        log.exception(value_error)
                        snapshot(browser)
                    except KeyError:
                        if log.level == 10:
                            log.warn('charts: include_screenshots_of_charts not set in yaml, defaulting to default screenshot')
                    data['screenshots_url'] = screenshots_url
                    data['screenshots'] = screenshots
                    data['filenames'] = filenames
                    data['asset'] = asset
                    if 'labels' in signal:
                        for label in signal['labels']:
                            for _key in label:
                                if not (_key in data):
                                    data[_key] = label[_key]
                    data['signal'] = signal
                    log.info('"{}" triggered'.format(signal['name']))
                    # log.info(signal['indicators'][0]['values'])
                    triggered_signals.append(data)
                total_alerts += 1

        if 'alerts' in chart:
            interval = get_interval(timeframe)
            for alert in chart['alerts']:
                if counter_alerts >= config.getint('tradingview', 'max_alerts') and config.getboolean('tradingview', 'clear_inactive_alerts'):
                    # try clean inactive alerts first
                    wait_and_click(browser, css_selectors['btn_calendar'])
                    wait_and_click(browser, css_selectors['btn_alerts'])
                    wait_and_click(browser, css_selectors['btn_alert_menu'])

                    try:
                        wait_and_click_by_text(browser, 'div', 'Delete all inactive')
                        wait_and_click(browser, css_selectors['btn_dlg_clear_alerts_confirm'])
                        time.sleep(DELAY_BREAK * 8)
                    except TimeoutException as e:
                        log.debug(e)

                    # update counter
                    alerts = find_elements(browser, css_selectors['item_alerts'])
                    if type(alerts) is list:
                        counter_alerts = len(alerts)
                    # close alerts tab
                    if find_element(browser, css_selectors['btn_alert_menu'], By.CSS_SELECTOR, False, True):
                        wait_and_click(browser, css_selectors['btn_alerts'])

                if counter_alerts >= config.getint('tradingview', 'max_alerts'):
                    log.warning("Maximum alerts reached. You can set this to a higher number in the kairos.cfg. Exiting program.")
                    return [counter_alerts, total_alerts]
                try:
                    screenshot_url = ''
                    if config.has_option('logging', 'screenshot_timing') and config.get('logging', 'screenshot_timing') == 'alert':
                        screenshot_url = take_screenshot(browser, symbol, interval)[0]
                    create_alert(browser, alert, timeframe, interval, symbol, screenshot_url)
                    counter_alerts += 1
                    total_alerts += 1
                except Exception as err:
                    log.error("Could not set alert: {} {}".format(symbol, alert['name']))
                    log.exception(err)
                    snapshot(browser)
    except Exception as e:
        log.exception(e)
        return retry_process_symbol(browser, chart, symbol, timeframe, counter_alerts, total_alerts, retry_number)
    return [counter_alerts, total_alerts]


def retry_process_symbol(browser, chart, symbol, timeframe, counter_alerts, total_alerts, retry_number=0):
    if retry_number < config.getint('tradingview', 'create_alert_max_retries'):
        log.info('trying again ({})'.format(str(retry_number + 1)))
        refresh(browser)
        try:
            # might be useful for multi threading set the symbol by going to different url like this:
            # https://www.tradingview.com/chart/?symbol=BINANCE%3AAGIBTC
            input_symbol = find_element(browser, css_selectors['input_symbol'])
            set_value(browser, input_symbol, symbol)
            input_symbol.send_keys(Keys.ENTER)
            # time.sleep(DELAY_CHANGE_SYMBOL)
        except Exception as err:
            log.debug('Unable to change to symbol')
            log.exception(err)
            snapshot(browser)
        return process_symbol(browser, chart, symbol, timeframe, counter_alerts, total_alerts, False, retry_number + 1)
    else:
        log.error('Max retries reached.')
        if symbol not in processing_errors:
            processing_errors.append(symbol)
        snapshot(browser)
        return False


def wait_until_chart_is_loaded(browser):
    # xpath_loading = "//*[matches(text(),'(loading|compiling|error)','i')]"
    xpath_loading = "//*[matches(text(),'(loading|compiling)','i')]"
    elem_loading = find_elements(browser, xpath_loading, By.XPATH, False, True, DELAY_BREAK_MINI)
    while elem_loading and len(elem_loading) > 0:
        elem_loading = find_elements(browser, xpath_loading, By.XPATH, False, DELAY_BREAK_MINI)


def snapshot(browser, quit_program=False, chart_only=True, name=''):
    global MAX_SCREENSHOTS_ON_ERROR
    if config.has_option('logging', 'screenshot_on_error') and config.getboolean('logging', 'screenshot_on_error') and MAX_SCREENSHOTS_ON_ERROR > 0:
        MAX_SCREENSHOTS_ON_ERROR -= 1
        filename = datetime.datetime.now().strftime('%Y%m%d_%H%M%S') + '.png'
        if name:
            filename = '{}_{}'.format(str(name), filename)
        if not os.path.exists('log'):
            os.mkdir('log')
        filename = os.path.join('log', filename)

        try:
            element = find_element(browser, 'html')
            location = element.location
            size = element.size
            browser.save_screenshot(filename)
            offset_left, offset_right, offset_top, offset_bottom = [0, 0, 0, 0]
            if config.has_option('logging', 'screenshot_offset_left'):
                offset_left = config.getint('logging', 'screenshot_offset_right')
            if config.has_option('logging', 'screenshot_offset_right'):
                offset_right = config.getint('logging', 'screenshot_offset_top')
            if config.has_option('logging', 'screenshot_offset_top'):
                offset_top = config.getint('logging', 'screenshot_offset_left')
            if config.has_option('logging', 'screenshot_offset_bottom'):
                offset_bottom = config.getint('logging', 'screenshot_offset_bottom')
            x = location['x'] + offset_left
            y = location['y'] + offset_top
            width = location['x'] + size['width'] + offset_right
            height = location['y'] + size['height'] + offset_bottom
            im = Image.open(filename)
            if chart_only:
                im = im.crop((int(x), int(y), int(width), int(height)))
            im.save(filename)
            log.error(str(filename))
        except Exception as take_screenshot_error:
            log.exception(take_screenshot_error)
        if quit_program:
            write_console_log(browser)
            exit(errno.EFAULT)


def take_screenshot(browser, symbol, interval, chart_only=True, tpl_strftime="%Y%m%d", retry_number=0):
    """
    Use selenium for a screenshot, or alternatively use TradingView's screenshot feature
    :param browser:
    :param symbol:
    :param interval:
    :param chart_only:
    :param tpl_strftime:
    :param retry_number:
    :return:
    """
    screenshot_url = ''
    filename = ''

    try:

        if config.has_option('tradingview', 'tradingview_screenshot') and config.getboolean('tradingview', 'tradingview_screenshot'):
            #  This alternative implementation for 'element.send_keys' does not work on Linux
            #  action = ActionChains(browser)
            #  action.send_keys(Keys.TAB)
            #  action.perform()
            html = find_element(browser, 'html')
            html.send_keys(Keys.ALT + "s")
            time.sleep(DELAY_SCREENSHOT_DIALOG)
            input_screenshot_url = find_element(html, css_selectors['dlg_screenshot_url'])
            screenshot_url = input_screenshot_url.get_attribute('value')
            #  This alternative implementation for 'element.send_keys' does not work on Linux
            #  action = ActionChains(browser)
            #  action.send_keys(Keys.TAB)
            #  action.perform()
            html.send_keys(Keys.ESCAPE)
            log.debug(screenshot_url)

        elif screenshot_dir != '':
            chart_dir = ''
            match = re.search(r"^.*chart.(\w+).*", browser.current_url)
            if re.Match:
                today_dir = os.path.join(screenshot_dir, datetime.datetime.today().strftime(tpl_strftime))
                if not os.path.exists(today_dir):
                    os.mkdir(today_dir)
                chart_dir = os.path.join(today_dir, match.group(1))
                if not os.path.exists(chart_dir):
                    os.mkdir(chart_dir)
                chart_dir = os.path.join(chart_dir, )
            filename = symbol.replace(':', '_') + '_' + str(interval) + '.png'
            filename = os.path.join(chart_dir, filename)
            elem_chart = find_element(browser, 'layout__area--center', By.CLASS_NAME)
            time.sleep(DELAY_SCREENSHOT)
            browser.save_screenshot(filename)

            if chart_only:
                location = elem_chart.location
                size = elem_chart.size
                offset_left, offset_right, offset_top, offset_bottom = [0, 0, 0, 0]
                if config.has_option('logging', 'screenshot_offset_left'):
                    offset_left = config.getint('logging', 'screenshot_offset_right')
                if config.has_option('logging', 'screenshot_offset_right'):
                    offset_right = config.getint('logging', 'screenshot_offset_top')
                if config.has_option('logging', 'screenshot_offset_top'):
                    offset_top = config.getint('logging', 'screenshot_offset_left')
                if config.has_option('logging', 'screenshot_offset_bottom'):
                    offset_bottom = config.getint('logging', 'screenshot_offset_bottom')
                x = location['x'] + offset_left
                y = location['y'] + offset_top
                width = location['x'] + size['width'] + offset_right
                height = location['y'] + size['height'] + offset_bottom
                im = Image.open(filename)
                im = im.crop((int(x), int(y), int(width), int(height)))
                im.save(filename)
            log.debug(filename)

    except Exception as take_screenshot_error:
        log.exception(take_screenshot_error)
        return retry_take_screenshot(browser, symbol, interval, chart_only, tpl_strftime, retry_number)
    if screenshot_url == '' and filename == '':
        return retry_take_screenshot(browser, symbol, interval, chart_only, tpl_strftime, retry_number)

    return [screenshot_url, filename]


def retry_take_screenshot(browser, symbol, interval, chart_only, tpl_strftime, retry_number=0):

    if retry_number + 1 == config.getint('tradingview', 'create_alert_max_retries'):
        log.info('trying again ({})'.format(str(retry_number + 1)))
        refresh(browser)
        try:
            input_symbol = find_element(browser, css_selectors['input_symbol'])
            set_value(browser, input_symbol, symbol)
            input_symbol.send_keys(Keys.ENTER)
            # time.sleep(DELAY_CHANGE_SYMBOL)
        except Exception as e:
            log.exception(e)
    elif retry_number < config.getint('tradingview', 'create_alert_max_retries'):
        log.info('trying again ({})'.format(str(retry_number + 1)))
        return take_screenshot(browser, symbol, interval, chart_only, tpl_strftime, retry_number + 1)
    else:
        log.warn('max retries reached')
        snapshot(browser)


def create_alert(browser, alert_config, timeframe, interval, symbol, screenshot_url='', retry_number=0):
    """
    Create an alert based upon user specified yaml configuration.
    :param browser:         The webdriver.
    :param alert_config:    The config for this specific alert.
    :param timeframe:       Timeframe, e.g. 1 day, 2 days, 4 hours, etc.
    :param interval:        TV's short format, e.g. 2 weeks = 2W, 1 day = 1D, 4 hours =4H, 5 minutes = 5M.
    :param symbol:          Ticker / Symbol, e.g. COINBASE:BTCUSD.
    :param screenshot_url:  URL of TV's screenshot feature
    :param retry_number:    Optional. Number of retries if for some reason the alert wasn't created.
    :return: true, if successful
    """
    # noinspection PyGlobalUndefined
    global alert_dialog
    global SEARCH_FOR_WARNING
    try:
        indicators_present = False
        i = 0
        while not indicators_present and i < 20:
            # TODO replace 'element.send_keys" with
            # action = ActionChains(browser)
            # action.send_keys(Keys.ALT + "a")
            # action.perform()
            html = find_element(browser, 'html')
            html.send_keys(Keys.ALT + "a")
            el_options = find_elements(browser, css_selectors['options_dlg_create_alert_first_row_first_item'], By.CSS_SELECTOR, False, False, 0.5)
            indicators_present = el_options is not None
            if not indicators_present:
                try:
                    wait_and_click(browser, css_selectors['btn_alert_cancel'], 0.1)
                except TimeoutException as e:
                    log.debug(e)
                time.sleep(1)
            i += 1

        alert_dialog = find_element(browser, class_selectors['form_create_alert'], By.CLASS_NAME)
        log.debug(str(len(alert_config['conditions'])) + ' yaml conditions found')

        # 1st row, 1st condition
        current_condition = 0
        css_1st_row_left = css_selectors['dlg_create_alert_first_row_first_item']
        try:
            wait_and_click(alert_dialog, css_1st_row_left)
        except Exception as alert_err:
            log.exception(alert_err)
            return retry(browser, alert_config, timeframe, interval, symbol, screenshot_url, retry_number)

        el_options = find_elements(alert_dialog, css_selectors['options_dlg_create_alert_first_row_first_item'])
        if not select(browser, alert_config, current_condition, el_options, symbol):
            return retry(browser, alert_config, timeframe, interval, symbol, screenshot_url, retry_number)

        # 1st row, 2nd condition (if applicable)
        css_1st_row_right = css_selectors['exists_dlg_create_alert_first_row_second_item']
        if element_exists(alert_dialog, css_1st_row_right, 0.5):
            current_condition += 1
            wait_and_click(alert_dialog, css_selectors['dlg_create_alert_first_row_second_item'])
            el_options = find_elements(alert_dialog, css_selectors['options_dlg_create_alert_first_row_second_item'])
            if not select(browser, alert_config, current_condition, el_options, symbol):
                return False

        # 2nd row, 1st condition
        current_condition += 1
        css_2nd_row = css_selectors['dlg_create_alert_second_row']
        wait_and_click(alert_dialog, css_2nd_row)
        el_options = find_elements(alert_dialog, css_selectors['options_dlg_create_alert_second_row'])
        if not select(browser, alert_config, current_condition, el_options, symbol):
            return False

        # 3rd+ rows, remaining conditions
        current_condition += 1
        i = 0
        while current_condition < len(alert_config['conditions']):
            time.sleep(DELAY_BREAK_MINI)
            log.debug('setting condition {0} to {1}'.format(str(current_condition + 1), alert_config['conditions'][current_condition]))
            # we need to get the inputs again for every iteration as the number may change
            inputs = find_elements(alert_dialog, css_selectors['inputs_and_selects_create_alert_3rd_row_and_above'])
            while True:
                if inputs[i].get_attribute('type') == 'hidden':
                    i += 1
                else:
                    break

            if inputs[i].tag_name == 'select':
                elements = find_elements(alert_dialog, css_selectors['dlg_create_alert_3rd_row_group_item'])
                if not ((elements[i].text == alert_config['conditions'][current_condition]) or ((not EXACT_CONDITIONS) and elements[i].text.startswith(alert_config['conditions'][current_condition]))):
                    elements[i].click()
                    time.sleep(DELAY_BREAK_MINI)

                    el_options = find_elements(elements[i], css_selectors['options_dlg_create_alert_3rd_row_group_item'])
                    condition_yaml = str(alert_config['conditions'][current_condition])
                    found = False
                    for j, option in enumerate(el_options):
                        option = el_options[j]
                        option_tv = str(option.get_attribute("innerHTML")).strip()
                        if (option_tv == condition_yaml) or ((not EXACT_CONDITIONS) and option_tv.startswith(condition_yaml)):
                            wait_and_click(alert_dialog, css_selectors['selected_dlg_create_alert_3rd_row_group_item'].format(j + 1))
                            found = True
                            break
                    if not found:
                        log.error("Invalid condition ({}): '{}' in yaml definition '{}'. Did the title/name of the indicator/condition change?".format(str(current_condition + 1), alert_config['conditions'][current_condition], alert_config['name']))
                        return False
            elif inputs[i].tag_name == 'input':
                set_value(browser, inputs[i], str(alert_config['conditions'][current_condition]).strip())

            # give some time
            current_condition += 1
            i += 1

        # Options (i.e. frequency)
        wait_and_click(alert_dialog, css_selectors['checkbox_dlg_create_alert_frequency'].format(str(alert_config['options']).strip()))
        # Expiration
        set_expiration(browser, alert_dialog, alert_config)

        # Toggle 'more actions'
        wait_and_click(alert_dialog, css_selectors['btn_toggle_more_actions'])

        # Show popup
        checkbox = find_element(alert_dialog, name_selectors['checkbox_dlg_create_alert_show_popup'], By.NAME)
        if is_checkbox_checked(checkbox) != alert_config['show_popup']:
            wait_and_click(alert_dialog, css_selectors['clickable_dlg_create_alert_show_popup'])

        # Sound
        checkbox = find_element(alert_dialog, name_selectors['checkbox_dlg_create_alert_play_sound'], By.NAME)
        if is_checkbox_checked(checkbox) != alert_config['sound']['play']:
            wait_and_click(alert_dialog, css_selectors['clickable_dlg_create_alert_play_sound'])
        if is_checkbox_checked(checkbox):
            # set ringtone
            wait_and_click(alert_dialog, css_selectors['dlg_create_alert_ringtone'])
            el_options = find_elements(alert_dialog, css_selectors['options_dlg_create_alert_ringtone'])
            for option in el_options:
                if str(option.text).strip() == str(alert_config['sound']['ringtone']).strip():
                    option.click()
            # set duration
            wait_and_click(alert_dialog, css_selectors['dlg_create_alert_sound_duration'])
            el_options = find_elements(alert_dialog, css_selectors['options_dlg_create_alert_sound_duration'])
            for option in el_options:
                if str(option.text).strip() == str(alert_config['sound']['duration']).strip():
                    option.click()

        # Communication options
        # Send Email
        try:
            checkbox = find_element(alert_dialog, name_selectors['checkbox_dlg_create_alert_send_email'], By.NAME)
            if is_checkbox_checked(checkbox) != alert_config['send']['email']:
                wait_and_click(alert_dialog, css_selectors['clickable_dlg_create_alert_send_email'])
            # Send Email-to-SMS (the checkbox is indeed called 'send-sms'!)
            checkbox = find_element(alert_dialog, name_selectors['checkbox_dlg_create_alert_email_to_sms'], By.NAME)
            if is_checkbox_checked(checkbox) != alert_config['send']['email-to-sms']:
                wait_and_click(alert_dialog, css_selectors['clickable_dlg_create_alert_send_email_to_sms'])
            # Send SMS (only for premium members)
            # checkbox = find_element(alert_dialog, name_selectors['checkbox_dlg_create_alert_send_sms'], By.NAME)
            # if is_checkbox_checked(checkbox) != alert_config['send']['sms']:
            #     wait_and_click(alert_dialog, css_selectors['clickable_dlg_create_alert_send_sms'])
            # Notify on App
            checkbox = find_element(alert_dialog, name_selectors['checkbox_dlg_create_alert_send_push'], By.NAME)
            if is_checkbox_checked(checkbox) != alert_config['send']['notify-on-app']:
                wait_and_click(alert_dialog, css_selectors['clickable_dlg_create_alert_send_push'])

            # Construct message
            chart = browser.current_url + '?symbol=' + symbol
            show_multi_chart_layout = False
            try:
                show_multi_chart_layout = alert_config['show_multi_chart_layout']
            except KeyError:
                log.warn('charts: multichartlayout not set in yaml, defaulting to multichartlayout = no')
            if type(interval) is str and len(interval) > 0 and not show_multi_chart_layout:
                chart += '&interval=' + str(interval)
            textarea = find_element(alert_dialog, 'description', By.NAME)
            """
            # This has stopped working. :( The text is visible but not set.           
            generated = textarea.text
            """
            # fall back to an empty generated text
            generated = ''
            text = str(alert_config['message']['text'])
            text = text.replace('%TIMEFRAME', ' ' + timeframe)
            text = text.replace('%SYMBOL', ' ' + symbol)
            text = text.replace('%NAME', ' ' + alert_config['name'])
            text = text.replace('%CHART', ' ' + chart)
            text = text.replace('%SCREENSHOT', ' ' + screenshot_url)
            text = text.replace('%GENERATED', generated)
            try:
                screenshot_urls = []
                for screenshot_chart in alert_config['include_screenshots_of_charts']:
                    screenshot_urls.append(str(screenshot_chart) + '?symbol=' + symbol)
                text += ' screenshots_to_include: ' + str(screenshot_urls).replace("'", "")
            except ValueError as value_error:
                log.exception(value_error)
                snapshot(browser)
            except KeyError:
                log.warn('charts: include_screenshots_of_charts not set in yaml, defaulting to default screenshot')
            set_value(browser, textarea, text, True)
        except Exception as alert_err:
            log.exception(alert_err)
            snapshot(browser)
            return retry(browser, alert_config, timeframe, interval, symbol, screenshot_url, retry_number)

        # Submit the form
        element = find_element(browser, css_selectors['btn_dlg_create_alert_submit'])
        element.click()
        # ignore warnings if they are there
        if SEARCH_FOR_WARNING:
            try:
                wait_and_click(browser, css_selectors['btn_create_alert_warning_continue_anyway'], 5)
                log.info('Warning found and closed')
            except TimeoutException:
                # we are getting a timeout exception because there likely was no warning
                log.info('No warning found when setting the alert.')
                SEARCH_FOR_WARNING = False

        time.sleep(DELAY_SUBMIT_ALERT)
    except TimeoutError:
        log.warn('time out')
        # on except, refresh and try again
        return retry(browser, alert_config, timeframe, interval, symbol, screenshot_url, retry_number)
    except Exception as exc:
        log.exception(exc)
        snapshot(browser)
        # on except, refresh and try again
        return retry(browser, alert_config, timeframe, interval, symbol, screenshot_url, retry_number)

    return True


def select(browser, alert_config, current_condition, el_options, ticker_id):
    log.debug('setting condition {0} to {1}'.format(str(current_condition + 1), alert_config['conditions'][current_condition]))
    value = str(alert_config['conditions'][current_condition])

    if value == "%SYMBOL":
        value = ticker_id.split(':')[1]

    found = False
    for option in el_options:
        option_tv = str(option.get_attribute("innerHTML")).strip()
        if (option_tv == value) or ((not EXACT_CONDITIONS) and option_tv.startswith(value)):
            hover(browser, option, True)
            found = True
            break
    if not found:
        log.error("Invalid condition ({}): '{}' in yaml definition '{}'. Did the title/name of the indicator/condition change?".format(str(current_condition + 1), alert_config['conditions'][current_condition], alert_config['name']))
    return found


def clear(element):
    element.clear()
    element.send_keys(SELECT_ALL)
    element.send_keys(Keys.DELETE)
    time.sleep(DELAY_BREAK_MINI * 0.5)


def send_keys(element, string, interval=DELAY_KEYSTROKE):
    if interval == 0:
        element.send_keys(string)
    else:
        for char in string:
            element.send_keys(char)
            time.sleep(interval)


def set_value(browser, element, string, use_clipboard=False, use_send_keys=False, interval=DELAY_KEYSTROKE):
    if use_send_keys:
        send_keys(element, string, interval)
    else:
        browser.execute_script("arguments[0].value = '{}';".format(string), element)
        if use_clipboard:
            if config.getboolean('webdriver', 'clipboard'):
                element.send_keys(SELECT_ALL)
                element.send_keys(CUT)
                element.send_keys(PASTE)
            else:
                send_keys(element, string, interval)


def retry(browser, alert_config, timeframe, interval, symbol, screenshot_url, retry_number=0):
    if retry_number < config.getint('tradingview', 'create_alert_max_retries'):
        log.info('trying again ({})'.format(str(retry_number + 1)))
        refresh(browser)
        try:
            # change symbol
            input_symbol = find_element(browser, css_selectors['input_symbol'])
            set_value(browser, input_symbol, symbol)
            input_symbol.send_keys(Keys.ENTER)
            time.sleep(DELAY_CHANGE_SYMBOL)
        except Exception as err:
            log.debug("Can't find {} in list of symbols" + str(symbol))
            log.exception(err)
        return create_alert(browser, alert_config, timeframe, interval, symbol, screenshot_url, retry_number + 1)
    else:
        log.error('Max retries reached.')
        snapshot(browser)
        return False


def is_checkbox_checked(checkbox):
    checked = False
    try:
        checked = checkbox.get_attribute('checked') == 'true'
    finally:
        return checked


def set_expiration(browser, _alert_dialog, alert_config):
    max_minutes = 86400
    datetime_format = '%Y-%m-%d %H:%M'

    exp = alert_config['expiration']
    if type(exp) is int:
        alert_config['expiration'] = dict()
        alert_config['expiration']['time'] = exp
        alert_config['expiration']['open-ended'] = False
    else:
        if 'time' not in alert_config['expiration']:
            alert_config['expiration']['time'] = exp
        if 'open-ended' not in alert_config['expiration']:
            alert_config['expiration']['open-ended'] = False

    checkbox = find_element(_alert_dialog, css_selectors['checkbox_dlg_create_alert_open_ended'])
    if is_checkbox_checked(checkbox) != alert_config['expiration']['open-ended']:
        wait_and_click(_alert_dialog, css_selectors['clickable_dlg_create_alert_open_ended'])

    if alert_config['expiration']['open-ended'] or str(alert_config['expiration']['time']).strip() == '' or str(alert_config['expiration']['time']).strip().lower().startswith('n') or type(alert_config['expiration']['time']) is None:
        return
    elif type(alert_config['expiration']['time']) is int:
        target_date = datetime.datetime.now() + datetime.timedelta(minutes=float(alert_config['expiration']['time']))
    elif type(alert_config['expiration']['time']) is str and len(str(alert_config['expiration']['time']).strip()) > 0:
        target_date = datetime.datetime.strptime(str(alert_config['expiration']['time']).strip(), datetime_format)
    else:
        return

    max_expiration = datetime.datetime.now() + datetime.timedelta(minutes=float(max_minutes - 1440))
    if target_date > max_expiration:
        target_date = max_expiration
    date_value = target_date.strftime('%Y-%m-%d')
    time_value = target_date.strftime('%H:%M')

    # For some reason TV does not register setting the date value directly.
    # Furthermore, we need to make sure that the date and time inputs are cleared beforehand.
    input_date = find_element(alert_dialog, 'alert_exp_date', By.NAME)
    clear(input_date)
    set_value(browser, input_date, date_value, False, True)
    input_time = find_element(_alert_dialog, 'alert_exp_time', By.NAME)
    time.sleep(DELAY_BREAK_MINI)
    clear(input_time)
    set_value(browser, input_time, time_value, False, True)
    send_keys(input_time, Keys.TAB)
    time.sleep(DELAY_BREAK_MINI)


def login(browser, uid='', pwd='', retry_login=False):
    global TV_UID
    global TV_PWD
    global ALREADY_LOGGED_IN
    if uid == '' and config.has_option('tradingview', 'username'):
        uid = config.get('tradingview', 'username')
    if pwd == '' and config.has_option('tradingview', 'password'):
        pwd = config.get('tradingview', 'password')

    if not retry_login:
        try:
            url = 'https://www.tradingview.com'
            browser.get(url)
            try:
                res = RESOLUTION.split(',')
                if len(res) >= 2:
                    browser.set_window_size(res[0], res[1])
                    # log.info("resolution set to " + str(res[0]) + 'x' + str(res[1]))
            except Exception as e:
                log.debug(e)

            # if logged in under a different username or not logged in at all log out and then log in again
            try:
                elem_username = wait_and_visible(browser, css_selectors['username'], 5)
                if type(elem_username) is WebElement:
                    if elem_username.get_attribute('textContent') != '' and elem_username.get_attribute('textContent') == uid:
                        ALREADY_LOGGED_IN = True
                        log.info("already logged in")
                        return True
                    else:
                        log.info("logged in under a different username. Logging out.")
                        wait_and_click(browser, css_selectors['username'])
                        wait_and_click(browser, css_selectors['signout'])
            except TimeoutException as e:
                log.debug(e)
            except Exception as e:
                log.exception(e)

        except Exception as e:
            log.exception(e)
            snapshot(browser, True)

    try:
        wait_and_click(browser, css_selectors['signin'])
        wait_and_click(browser, css_selectors['show_email_or_username'])
        input_username = find_element(browser, css_selectors['input_username'])
        if input_username.get_attribute('value') == '' or retry_login:
            while uid == '':
                uid = input("type your TradingView username and press enter: ")

        input_password = find_element(browser, css_selectors['input_password'])
        if input_password.get_attribute('value') == '' or retry_login:
            while pwd == '':
                pwd = getpass.getpass("type your TradingView password and press enter: ")

        # set credentials on website login page
        if uid != '' and pwd != '':
            set_value(browser, input_username, uid)
            time.sleep(DELAY_BREAK_MINI)
            set_value(browser, input_password, pwd)
            time.sleep(DELAY_BREAK_MINI)
        # if there are no user credentials then exit
        else:
            log.info("no credentials provided.")
            write_console_log(browser)
            exit(0)

        wait_and_click(browser, css_selectors['btn_login'])

    except Exception as e:
        log.error(e)
        snapshot(browser, True)

    try:
        elem_username = wait_and_get(browser, css_selectors['username'])
        if type(elem_username) is WebElement and elem_username.get_attribute('textContent') != '' and elem_username.get_attribute('textContent') == uid:
            TV_UID = uid
            TV_PWD = pwd
            log.info("logged in successfully at tradingview.com as {}".format(elem_username.get_attribute('textContent')))
        else:
            if elem_username.get_attribute('textContent') == '' or elem_username.get_attribute('textContent') == 'Guest':
                log.warn("not logged in at tradingview.com")
            elif elem_username.get_attribute('textContent') != uid:
                log.warn("logged in under a different username at tradingview.com")
            error = find_element(browser, 'body > div.tv-dialog__modal-wrap > div > div > div > div.tv-dialog__error.tv-dialog__error--dark')
            if error:
                print(error.get_attribute('innerText'))
                login(browser, '', '', True)
    except Exception as e:
        log.error(e)
        snapshot(browser, True)
    close_cookies_message(browser)


def assign_user_data_directory():
    lockfile = 'lockfile'
    if OS != 'windows':
        lockfile = 'SingletonSocket'

    user_data_directory = config.get('webdriver', 'user_data_directory').strip()
    user_data_base_dir, tail = os.path.split(user_data_directory)
    kairos_data_directory = os.path.join(user_data_base_dir, 'kairos')
    if kairos_data_directory == user_data_directory:
        log.critical("{} is reserved as a backup to create new user data directories from. Please, set a different user data directory under [webdriver] -> kairos_data_directory and restart Kairos.")
        exit(1)
    if not os.path.exists(kairos_data_directory):
        if os.path.isfile(os.path.join(user_data_directory, lockfile)) or os.path.islink(os.path.join(user_data_directory, lockfile)):
            log.critical("Your user data directory is locked. Please close your browser and restart Kairos.")
            exit(1)
        # create new user data directory for Kairos
        log.info("creating base user data directory 'kairos'. Please be patient while data is being copied ...")
        shutil.copytree(user_data_directory, kairos_data_directory)
        return kairos_data_directory, True
        # tools.chmod_r(kairos_data_directory, 0o777)

    # user_data_directory = kairos_data_directory
    if config.has_option('webdriver', 'share_user_data') and config.getboolean('webdriver', 'share_user_data'):
        log.debug("{} in use? {}".format(user_data_directory, os.path.exists(os.path.join(user_data_directory, lockfile))))
        user_data_directory_found = False

        # find an unused kairos user data directory
        user_data_base_dir, tail = os.path.split(user_data_directory)
        try:
            with os.scandir(user_data_base_dir) as user_data_directories:
                # number_of_kairos_user_data_directories = 0
                for entry in user_data_directories:
                    if entry.name.startswith('kairos_'):
                        # number_of_kairos_user_data_directories += 1
                        path = os.path.join(user_data_base_dir, entry)
                        if not tools.path_in_use(path, log) and not user_data_directory_found:
                            user_data_directory = path
                            user_data_directory_found = True
                            break

                # make a copy of the default user data directory if it is not found
                i = 0
                while not user_data_directory_found and i < 100:
                    path = os.path.join(user_data_base_dir, "kairos_{}".format(i))
                    if not os.path.exists(path):
                        user_data_directory_found = True
                        log.info("creating user data directory 'kairos_{}'. Please be patient while data is being copied ...".format(i))
                        shutil.copytree(kairos_data_directory, path)
                        if OS == 'linux':
                            tools.chmod_r(path, 0o777)
                        user_data_directory = path
                    i += 1
        except Exception as e:
            log.exception(e)

    user_data_base_dir, name = os.path.split(user_data_directory)
    log.info("{} assigned".format(name))
    return r"" + str(user_data_directory), False


def check_driver(driver):
    driver_version = ""
    browser_version = driver.capabilities['browserVersion']

    if driver.name in driver.capabilities:
        for key, value in driver.capabilities[driver.name].items():
            match = re.search(r"version", key, re.IGNORECASE)
            if match:
                match = re.search(r"([\d+.]+\d+) ", value)
                if match:
                    driver_version = match.group(1).rstrip()
                break
    else:
        log.warn("browser name '{}' not found in driver".format(driver.name))
    log.info("browser version: {}".format(browser_version))
    log.info("driver version: {}".format(driver_version))

    # driver_version_major = driver_version.split('.')[0]
    # browser_version_major = browser_version.split('.')[0]
    # if driver_version_major != browser_version_major:
    #     subject = "Outdated web driver"
    #     text = "Please update your web driver.\n\nWeb driver version: {}\nBrowser version: {}".format(driver_version, browser_version)
    #     # Send email
    #     import mail
    #     mail.send_admin_message(subject, text)


def create_browser(run_in_background):
    global log
    capabilities = DesiredCapabilities.CHROME.copy()
    initial_setup = False

    options = webdriver.ChromeOptions()
    # options.add_argument("--incognito")
    if config.has_option('webdriver', 'web_browser_path'):
        web_browser_path = r"" + str(config.get('webdriver', 'web_browser_path'))
        options.binary_location = web_browser_path
    if OS == 'linux':
        options.add_argument('--no-sandbox')
        options.add_argument("--disable-dev-shm-usage")
    if config.has_option('webdriver', 'user_data_directory') and config.get('webdriver', 'user_data_directory').strip() != "":
        kairos_data_directory, initial_setup = assign_user_data_directory()
        match = re.search(r".*(\d+)", kairos_data_directory)
        if match:
            instance = match.group(1)
            fn = tools.debug.file_name
            match = re.search(r".*(\..*)", tools.debug.file_name)
            if match:
                fn = fn.replace(match.group(1), "_{}{}".format(instance, match.group(1)))
            tools.shutdown_logging()
            tools.debug.file_name = fn
            log = tools.create_log()

        options.add_argument('--user-data-dir=' + kairos_data_directory)
        match = re.search(r".*(\d+)", kairos_data_directory)
        if match:
            global WEBDRIVER_INSTANCE
            WEBDRIVER_INSTANCE = int(match.group(1))

    options.add_argument('--disable-extensions')
    options.add_argument('--disable-notifications')
    options.add_argument('--noerrdialogs')
    options.add_argument('--disable-session-crashed-bubble')
    # options.add_argument('--disable-infobars')
    # options.add_argument('--disable-restore-session-state')
    options.add_argument('--window-size=' + RESOLUTION)
    # suppress the INFO:CONSOLE messages
    options.add_argument("--log-level=3")

    prefs = {
        'profile.default_content_setting_values.notifications': 2
        # , 'disk-cache-size': 52428800
    }
    options.add_experimental_option('prefs', prefs)
    exclude_switches = [
        'enable-automation',
    ]
    options.add_experimental_option('excludeSwitches', exclude_switches)
    # fix gpu_process_transport)factory.cc(980) error on Windows when in 'headless' mode, see:
    # https://stackoverflow.com/questions/50143413/errorgpu-process-transport-factory-cc1007-lost-ui-shared-context-while-ini
    if OS == 'windows':
        options.add_argument('--disable-gpu')
    # run chrome in the background
    if run_in_background:
        options.add_argument('--headless')
    browser = None

    chromedriver_file = r"" + str(config.get('webdriver', 'path'))
    if not os.path.exists(chromedriver_file):
        log.error("File {} does not exist. Did setup your kairos.cfg correctly?".format(chromedriver_file))
        raise FileNotFoundError
    chromedriver_file.replace('.exe', '')

    # use open chrome browser
    # options = webdriver.ChromeOptions()
    # options.add_experimental_option("debuggerAddress", "127.0.0.1:9222")

    try:
        # noinspection PyUnboundLocalVariable
        log_path = r"--log-path=.\chromedriver_{}.log".format(int(WEBDRIVER_INSTANCE))

        # Create webdriver.remote
        # Note, we cannot serialize webdriver.Chrome
        if MULTI_THREADING:
            browser = webdriver.Remote(command_executor=EXECUTOR, options=options, desired_capabilities=capabilities)
        else:
            browser = webdriver.Chrome(executable_path=chromedriver_file, options=options, desired_capabilities=capabilities, service_args=["--verbose", log_path])

        check_driver(browser)

        browser.implicitly_wait(WAIT_TIME_IMPLICIT)
        browser.set_page_load_timeout(PAGE_LOAD_TIMEOUT)
        if initial_setup:
            log.info("creating shared session for kairos user data directory")
            login(browser)
            global ALREADY_LOGGED_IN
            ALREADY_LOGGED_IN = True
            destroy_browser(browser, False)
            log.info("restarting kairos ... ")
            return create_browser(run_in_background)
    except InvalidArgumentException as e:
        if e.msg.index("user data directory is already in use") >= 0:
            log.critical("your web browser's user data directory is in use. Please, close your web browser and restart Kairos.")
            exit(0)
        else:
            log.exception(e)
    except SessionNotCreatedException as e:
        index = 0
        if 'session not created: ' in e.msg:
            index = len('session not created: ')
        error = e.msg[index:]

        if "chrome" in error.lower():
            subject = "Outdated Chromedriver"
            text = "Could not run due to an outdated Chromedriver.\nPlease update your Chromedriver."
            log.error("Please update Chromedriver. {}".format(error))
        else:
            subject = "Outdated Geckodriver"
            text = "Could not run due to run due to an outdated Geckodriver.\nPlease update your Geckodriver."
            log.error("Please update Geckodriver. {}".format(error))

        # Send email
        import mail
        # TODO: make sure to send it only once per day
        mail.send_admin_message(subject, text)
        exit(0)
    except Exception as e:
        log.exception(e)
        exit(1)

    return browser


def save_browser_state(browser):
    # Serialize and save on disk
    fp = open(FILENAME, 'wb')
    # pickle()
    dill.dump(browser, fp)
    fp.close()


def get_browser_instance(browser=None):
    result = browser
    if os.path.exists(FILENAME):
        result = dill.load(open(FILENAME, 'rb'))
    return result


def logout(browser):
    try:
        browser.switch_to.window(browser.window_handles[0])
        wait_and_click(browser, css_selectors['btn_user_menu'])
        wait_and_click(browser, css_selectors['btn_logout'])
        log.info("logged out of TradingView")
    except Exception as e:
        log.exception(e)
        snapshot(browser)


def destroy_browser(browser, close_log=True):
    try:
        if type(browser) is webdriver.Chrome:
            close_all_popups(browser)
            share_user_data = config.has_option('webdriver', 'share_user_data') and config.getboolean('webdriver', 'share_user_data')
            if not ALREADY_LOGGED_IN and not share_user_data:
                logout(browser)
            write_console_log(browser)
            if close_log:
                tools.shutdown_logging()
    except Exception as e:
        log.exception(e)
        snapshot(browser)
    finally:
        browser.close()
        browser.quit()


def write_console_log(browser):
    write_mode = 'a'
    if config.getboolean('logging', 'clear_on_start_up'):
        write_mode = 'w'
    tools.write_console_log(browser, write_mode)


def run(file, export_signals_immediately, multi_threading=False):
    """
        TODO:   multi threading
    """
    log.info("Running on a {} operating system".format(OS))
    counter_alerts = 0
    total_alerts = 0
    browser = None

    global RUN_IN_BACKGROUND
    global MULTI_THREADING
    global WEBDRIVER_INSTANCE
    MULTI_THREADING = multi_threading

    save_as = ""
    try:
        if len(file) > 1:
            head, tail = os.path.split(r""+file)
            save_as = tail
            file = r"" + os.path.join(config.get('tradingview', 'settings_dir'), file)
        else:
            file = r"" + os.path.join(config.get('tradingview', 'settings_dir'), config.get('tradingview', 'settings'))
        if not os.path.exists(file):
            log.error("File {} does not exist. Did you setup your kairos.cfg and yaml file correctly?".format(str(file)))
            raise FileNotFoundError

        # get the user defined yaml file
        tv = tools.get_yaml_config(file, log, True)
        has_charts = 'charts' in tv
        has_screeners = 'screeners' in tv

        RUN_IN_BACKGROUND = config.getboolean('webdriver', 'run_in_background')
        if 'webdriver' in tv and 'run-in-background' in tv['webdriver']:
            RUN_IN_BACKGROUND = tv['webdriver']['run-in-background']

        if has_screeners or has_charts:
            browser = create_browser(RUN_IN_BACKGROUND)
            login(browser, TV_UID, TV_PWD)
            if has_screeners:
                try:
                    max_symbols_per_watchlist = 1000  # TV limit

                    screeners_yaml = tv['screeners']
                    for screener_yaml in screeners_yaml:
                        if (not ('enabled' in screener_yaml)) or screener_yaml['enabled']:
                            log.info("extracting symbols from screener '{}'. Please be patient, this may take a minute or two ...".format(screener_yaml['name']))
                            markets = get_screener_markets(browser, screener_yaml)
                            if markets:
                                markets.sort()
                                chunks = tools.chunks(markets, max_symbols_per_watchlist)
                                number_of_chunks = len(markets) // max_symbols_per_watchlist + 1
                                name = screener_yaml['name'].strip()
                                for i, chunk in enumerate(chunks):
                                    if i > 0:
                                        name = "{} {}/{}".format(screener_yaml['name'], str(i+1), str(number_of_chunks))
                                    if update_watchlist(browser, name, chunk):
                                        log.info('watchlist {} updated ({} markets)'.format(screener_yaml['name'], str(len(chunk))))
                                # remove excess pagination watchlists, e.g. 4/5 and 5/5 when there are only 3 chunks with this update
                                if number_of_chunks == 1:
                                    name = screener_yaml['name'].strip() + " 1/1"
                                wait_and_click(browser, css_selectors['btn_calendar'])
                                time.sleep(DELAY_BREAK)
                                wait_and_click(browser, css_selectors['btn_watchlist'])
                                time.sleep(DELAY_BREAK)
                                remove_watchlists(browser, name, number_of_chunks+1)
                            else:
                                log.info('no markets to update')
                except Exception as e:
                    log.exception(e)
                    snapshot(browser)

            if has_charts:
                # do some maintenance on the alert list (removing or restarting)
                try:
                    if config.getboolean('tradingview', 'clear_alerts'):
                        wait_and_click(browser, css_selectors['btn_calendar'])
                        wait_and_click(browser, css_selectors['btn_alerts'])
                        wait_and_click(browser, css_selectors['btn_alert_menu'])

                        try:
                            wait_and_click_by_text(browser, 'div', 'Delete all', '', CHECK_IF_EXISTS_TIMEOUT, 1)
                            wait_and_click(browser, css_selectors['btn_dlg_clear_alerts_confirm'])
                            time.sleep(DELAY_BREAK * 2)
                        except TimeoutException as e:
                            log.debug(e)

                    else:
                        if config.getboolean('tradingview', 'restart_inactive_alerts'):
                            wait_and_click(browser, css_selectors['btn_calendar'])
                            wait_and_click(browser, css_selectors['btn_alerts'])
                            wait_and_click(browser, css_selectors['btn_alert_menu'])
                            # apparently, TV decided in all their wisdom to use a completely different structure for when you are on a chart vs e.g. the front page
                            # note the camel case when we are on the chart, and lack thereof on the startpage *facepalm*
                            try:
                                # check if we are on the front page
                                wait_and_click_by_text(browser, 'div', 'Restart all inactive')
                                wait_and_click(browser, css_selectors['btn_dlg_clear_alerts_confirm'])
                                time.sleep(DELAY_BREAK * 2)
                            except TimeoutException as e:
                                log.debug(e)

                        elif config.getboolean('tradingview', 'clear_inactive_alerts'):
                            wait_and_click(browser, css_selectors['btn_calendar'])
                            wait_and_click(browser, css_selectors['btn_alerts'])
                            wait_and_click(browser, css_selectors['btn_alert_menu'])

                            try:
                                wait_and_click_by_text(browser, 'div', 'Delete all inactive')
                                wait_and_click(browser, css_selectors['btn_dlg_clear_alerts_confirm'])
                                time.sleep(DELAY_BREAK * 2)
                            except TimeoutException as e:
                                log.debug(e)

                        # count the number of existing alerts
                        alerts = find_elements(browser, css_selectors['item_alerts'], By.CSS_SELECTOR, False)
                        if isinstance(alerts, list):
                            counter_alerts = len(alerts)
                except Exception as e:
                    log.exception(e)
                # iterate over all items that have an 'alerts' or 'signals' property
                for file, items in tv.items():
                    if type(items) is list:
                        for item in items:
                            if 'alerts' in item or 'signals' in item or 'strategies' in item:
                                [counter_alerts, total_alerts] = open_chart(browser, item, save_as, counter_alerts, total_alerts)

                if len(processing_errors) > 0:
                    subject = 'Kairos error report'
                    text = 'Unfortunately, Kairos could not screen the following markets.\n\n' + ', '.join(processing_errors) + '\n\nPlease review your log for additional clues.\n'
                    # Send email
                    import mail
                    mail.send_admin_message(subject, text)

                log.info(summary(total_alerts))
                print()
                if len(triggered_signals) > 0:
                    from tv import mail
                    mail.post_process_signals(triggered_signals)
                    if export_signals_immediately:
                        if 'summary' in tv:
                            mail.send_mail(browser, tv['summary'], triggered_signals, False)
                            # we've send the signals, let's make sure they aren't send a 2nd time
                            triggered_signals.clear()
                        else:
                            log.warn('No summary configuration found in {}. Unable to create a summary and to export data.'.format(str(file)))
                elif export_signals_immediately:
                    log.info('No signals triggered. Nothing to send')
                destroy_browser(browser)
    except Exception as exc:
        log.exception(exc)
        summary(total_alerts)
        destroy_browser(browser)
    return triggered_signals


def get_screener_markets(browser, screener_yaml):
    markets = []

    close_all_popups(browser)
    url = unquote(screener_yaml['url'])
    browser.get(url)
    loaded = False
    max_runs = 1000
    counter = 0
    found = False
    while not loaded and counter < max_runs:
        try:
            wait_and_click(browser, css_selectors['select_screener'], 30)
            el_options = find_elements(browser, css_selectors['options_screeners'])
            for i in range(len(el_options)):
                option = el_options[i]
                try:
                    log.debug(option.text)
                    if str(option.text) == screener_yaml['name']:
                        option.click()
                        loaded = True
                        found = True
                        break
                except StaleElementReferenceException:
                    el_options = find_elements(browser, css_selectors['options_screeners'])
                i += 1
        except ElementClickInterceptedException:
            time.sleep(0.1)
            pass
        except StaleElementReferenceException:
            pass
        counter += 1

    if not found:
        log.warn("screener '{}' doesn't exist.".format(screener_yaml['name']))
        return False

    if 'search' in screener_yaml and screener_yaml['search'] != '' and screener_yaml['search'] is not None:
        search_box = find_element(browser, css_selectors['input_screener_search'])
        set_value(browser, search_box, screener_yaml['search'], True)
        time.sleep(DELAY_SCREENER_SEARCH)

    # sort first, otherwise scrolling doesn't work
    # sort descending on the ticker column
    wait_and_click(browser, 'tv-screener-table__field-value--total', locator_strategy=By.CLASS_NAME)
    time.sleep(DELAY_BREAK * 4)

    # the list is ordered
    last_symbol = ""
    while last_symbol == "":
        try:
            first_row = find_element(browser, '//*[@id="js-screener-container"]/div[4]/table/tbody/tr[1]', By.XPATH)
            last_symbol = first_row.get_attribute('data-symbol')
        except StaleElementReferenceException:
            pass
        except Exception as e:
            log.exception(e)
            break
    log.debug("last_symbol = {}".format(last_symbol))
    # sort ascending on the ticker column
    wait_and_click(browser, 'tv-screener-table__field-value--total', locator_strategy=By.CLASS_NAME)
    time.sleep(DELAY_BREAK * 4)

    # move to the first row
    run_again = True
    while run_again:
        run_again = False
        try:
            ActionChains(browser).move_to_element(find_element(browser, '//*[@id="js-screener-container"]/div[4]/table/tbody/tr[1]', By.XPATH, False, True)).perform()
        except StaleElementReferenceException:
            run_again = True
        except Exception as e:
            log.exception(e)

    # get total found
    el_total_found = find_element(browser, 'tv-screener-table__field-value--total', By.CLASS_NAME)
    total_found = 0
    try:
        match = re.search(r"(\d+)", el_total_found.text)
        total_found = int(match.group(1))
    except StaleElementReferenceException:
        pass
    log.debug("found {} markets for screener '{}'".format(total_found, screener_yaml['name']))

    symbol = ""
    row_height = 50
    scroll_factor = 100
    dots = 0
    while symbol != last_symbol:
        dots = print_dot(dots)
        try:
            browser.execute_script("window.scrollBy(0, {});".format(row_height * scroll_factor))
            last_row = find_element(browser, '//*[@id="js-screener-container"]/div[4]/table/tbody/tr[last()]', By.XPATH)
            symbol = last_row.get_attribute('data-symbol')
            # move to the last row
            ActionChains(browser).move_to_element(last_row).perform()
        except StaleElementReferenceException:
            pass

    tries = 0
    max_tries = 3
    while len(markets) < total_found and tries < max_tries:
        tries = tries + 1

        rows = find_elements(browser, class_selectors['rows_screener_result'], By.CLASS_NAME, True, False, 10)

        i = 0
        while i < len(rows):
            if i > 0 and i % 40 == 0:
                dots = print_dot(dots)
            market = ""
            try:
                market = rows[i].get_attribute('data-symbol')
                # log.info(market)
            except StaleElementReferenceException:
                pass
            except IndexError as e:
                log.exception(e)
            i += 1
            markets.append(market)

        markets = list(set(markets))
    print(' DONE')
    log.info('extracted {} markets'.format(str(len(markets))))
    return markets


def update_watchlist(browser, name, markets):
    try:
        if isinstance(markets, str):
            markets = markets.split(',')
        log.info("updating {} with {} markets. Please be patient, this will take a while (100 markets/min or so) ...".format(name, len(markets)))

        wait_and_click(browser, css_selectors['btn_calendar'])
        time.sleep(DELAY_BREAK)
        wait_and_click(browser, css_selectors['btn_watchlist'])
        time.sleep(DELAY_BREAK)
        wait_and_click(browser, css_selectors['btn_watchlist_submenu'])
        time.sleep(DELAY_BREAK)
        input_symbol = find_element(browser, css_selectors['input_watchlist_add_symbol'])

        wait_and_click_by_text(browser, 'div', 'Create new list')
        time.sleep(DELAY_BREAK)

        css = '#overlap-manager-root > div > div > div.tv-dialog__scroll-wrap.i-with-actions > div > div > div > label > input'
        input_watchlist_name = find_element(browser, css)
        set_value(browser, input_watchlist_name, name)
        input_watchlist_name.send_keys(Keys.ENTER)
        time.sleep(DELAY_BREAK)

        added, missing = add_markets_to_watchlist(browser, input_symbol, markets)
        time.sleep(2)

        # how many were added?
        if len(missing) > 0:
            log.warn("unable to add the following markets: {}".format(", ".join(markets)))

        # sort the watchlist
        try:
            wait_and_click_by_text(browser, 'span', 'Symbol')
            time.sleep(DELAY_BREAK * 2)
        except Exception as e:
            log.exception(e)

        # remove double watchlist
        remove_watchlists(browser, name)
        return True
    except Exception as e:
        log.exception(e)
        snapshot(browser)


def add_markets_to_watchlist(browser, input_symbol, markets):
    added = 0
    dots = 0
    missing = []
    for market in markets:
        dots = print_dot(dots)
        if add_market_to_watchlist(browser, input_symbol, market):
            added += 1
        else:
            missing.append(market)
    print(" DONE")
    return added, missing


def add_market_to_watchlist(browser, input_symbol, market, tries=0):
    set_value(browser, input_symbol, market)
    input_symbol.send_keys(Keys.ENTER)

    added = element_exists(browser, 'div[data-symbol-full="{}"]'.format(market))
    if not added:
        tries += 1
        max_tries = max(config.getint('tradingview', 'create_alert_max_retries'), 10)
        if tries <= max_tries:
            added = add_market_to_watchlist(browser, input_symbol, market, tries)
            if log.level == DEBUG:
                print("")
                log.debug("{} trying again... ({}/{})".format(market, tries, max_tries))
    return added


def remove_watchlists(browser, name, from_pagination_page=0):
    """
    Removes old watchlists.
    @param browser
    @param name, the name of the watchlist including pagination (if any). For example, BTC markets, BTC markets 2/3
    @param from_pagination_page, when higher than 0, this method will remove all watchlists with a pagination page higher or equal. For example, with a from_pagination_page of 3, BTC markets 3/4 and 4/4 will be removed but BTC markets 2/4 will not be removed.
    """
    # After a watchlist is imported, TV opens it. Since we cannot delete a watchlist while opened, we can safely assume that any watchlist of the same name that can be deleted is old and should be deleted
    el_options = []
    try:
        # make sure we hover over the element to hide any tooltips of other elements
        hover(browser, find_element(browser, css_selectors['btn_watchlist_submenu']))
        time.sleep(DELAY_BREAK)
        wait_and_click(browser, css_selectors['btn_watchlist_submenu'])
        time.sleep(DELAY_BREAK*4)
        el_options = find_elements(browser, css_selectors['options_watchlist'])
    except Exception as e:
        log.exception(e)
        snapshot(browser)

    page = 0
    basename = name.strip()
    match = re.search(rf"^(.+)\s(\d+)/(\d+)$", name)
    if match:
        if match[1]:
            basename = match[1].strip()
        if match[2]:
            page = int(match[2])

    regex = rf"^{basename}$"
    if from_pagination_page:
        regex = rf"^{basename}\s+(\d+)/(\d+)$"
    elif page:
        regex = rf"^{basename}\s+({page})/(\d+)$"

    # remove all watch lists with the name, and with the name followed by pagination
    # e.g. BTC markets, BTC markets 2/3 and BTC markets 3/3
    j = 0
    while j < len(el_options):
        try:
            option_title = str(el_options[j].text)
            match = re.search(regex, option_title)

            if (match and from_pagination_page == 0) or (match and match.lastindex and int(match.group(1)) >= from_pagination_page > 0):
                log.debug("found match for {} with regex {} -> {}".format(name, regex, option_title))
                # get the removal button
                # the active watchlist doesn't have a remove button, so we need to check if it is actually there
                btn_delete = find_element(el_options[j], 'span[class^="removeButton"]', By.CSS_SELECTOR, False, False, 1)
                if btn_delete:
                    # hover over element and click the removal button [x]
                    hover(browser, btn_delete, True)
                    # handle confirmation dialog
                    time.sleep(0.5)
                    try:
                        wait_and_click(browser, 'div.js-dialog__action-click.js-dialog__no-drag.tv-button.tv-button--success')
                    except TimeoutException as e:
                        log.debug(e)
                    time.sleep(1)
                    # give TV time to remove the watchlist
                    log.debug('watchlist {} removed'.format(name))
        except StaleElementReferenceException:
            # open the watchlists menu again and update the options to prevent 'element is stale' error
            wait_and_click(browser, css_selectors['btn_watchlist_submenu'])
            time.sleep(DELAY_BREAK)
            el_options = find_elements(browser, css_selectors['options_watchlist'])
            time.sleep(DELAY_BREAK)
            j = 0
        except Exception as e:
            log.exception(e)
            snapshot(browser)
        j = j + 1


def open_performance_summary_tab(browser):
    try:
        # open strategy tab
        strategy_tab = find_element(browser, css_selectors['tab_strategy_tester_inactive'], By.CSS_SELECTOR, False, False,
                                    1)
        if isinstance(strategy_tab, WebElement):
            strategy_tab.click()

        # open performance summary tab
        max_tries = max(config.getint('tradingview', 'create_alert_max_retries'), 10)
        tries = 0
        while tries < max_tries:
            # noinspection PyBroadException
            try:
                strategy_performance_strategy_tab = find_element(browser,
                                                                 css_selectors['tab_strategy_tester_performance_summary'],
                                                                 By.CSS_SELECTOR, True, False, 2)
                if isinstance(strategy_performance_strategy_tab, WebElement):
                    strategy_performance_strategy_tab.click()
                tries = max_tries
            except Exception as e:
                log.exception(e)
                tries += 1

    except Exception as e:
        log.exception(e)


def get_strategy_default_values(browser, retry_number=0):
    try:
        # open dialog
        wait_and_click(browser, css_selectors['btn_strategy_dialog'])
        # click and set inputs
        wait_and_click(browser, css_selectors['indicator_dialog_tab_inputs'])
        inputs = get_indicator_dialog_values(browser)
        # click and set properties
        wait_and_click(browser, css_selectors['indicator_dialog_tab_properties'])
        properties = get_indicator_dialog_values(browser)
        # click OK
        wait_and_click(browser, css_selectors['btn_indicator_dialog_ok'])
    except Exception as e:
        return retry_get_strategy_default_values(browser, e, retry_number)
    return inputs, properties


def retry_get_strategy_default_values(browser, e, retry_number=0):
    max_tries = config.getint('tradingview', 'create_alert_max_retries')
    if retry_number < max_tries:
        return get_strategy_default_values(browser, retry_number+1)
    else:
        log.exception(e)
        return {}, {}


def get_indicator_dialog_values(browser):
    # get input titles
    result = dict()
    try:
        cells = find_elements(browser, css_selectors['indicator_dialog_tab_cells'])
        for i, cell in enumerate(cells):
            css_class = cell.get_attribute("class")
            if str(css_class).find('last') > 0:
                title = re.sub(r"[\W]", '', cells[i-1].text.replace(' ', '_')).lower()
                value = cell.text

                css = css_selectors['indicator_dialog_tab_cell'].format(i + 1) + ' input'
                css_labels = css_selectors['indicator_dialog_tab_cell'].format(i + 1) + ' label > span > span'
                inputs = find_elements(browser, css, By.CSS_SELECTOR, False, False, 1)
                labels = find_elements(browser, css_labels, By.CSS_SELECTOR, False, False, 1)
                # one or more checkboxes
                if labels and inputs:
                    for j, label in enumerate(labels):
                        value = ""
                        title += "_" + re.sub(r"[\W]", '', label.text.replace(' ', '_')).lower()
                        if inputs[j].get_attribute('type') == "checkbox":
                            if is_checkbox_checked(inputs[j]):
                                value = 'yes'
                            else:
                                value = 'no'
                        result[title] = value
                    continue
                elif inputs:
                    value = ""
                    for input_element in inputs:
                        value += input_element.get_attribute("value") + " "
                    if value:
                        value += cell.text

                if value:
                    result[title] = value.strip()
            elif str(css_class).find('fill') > 0:
                # all elements that have class '...fill...' are checkboxes
                title = re.sub(r"[\W]", '', cell.text.replace(' ', '_')).lower()
                css = css_selectors['indicator_dialog_tab_cell'].format(i + 1) + ' input'
                input_element = find_element(browser, css, By.CSS_SELECTOR, False, False, 1)
                if input_element:
                    if input_element.get_attribute('type') == "checkbox":
                        if is_checkbox_checked(input_element):
                            value = 'yes'
                        else:
                            value = 'no'
                    else:
                        value = input_element.get_attribute("value")
                else:
                    value = cell.text
                if value:
                    result[title] = value.strip()
            else:
                continue
    except StaleElementReferenceException:
        pass
    except Exception as e:
        log.exception(e)
    return result


def back_test(browser, strategy_config, symbols, atomic_inputs, atomic_properties):
    try:

        summaries = list()
        name = strategy_config['name']

        try:
            css = 'div.chart-container'
            number_of_charts = find_elements(browser, css)
            number_of_charts = len(number_of_charts)
        except TimeoutException:
            number_of_charts = 1
        log.info("Found {} charts on the layout".format(number_of_charts))

        number_of_strategies = max(len(atomic_properties), 1) * max(len(atomic_inputs), 1)
        # Both inputs and properties have been defined
        if len(atomic_properties) > 0 and len(atomic_inputs) > 0:
            log.info("Back testing {} with {} input sets and {} property sets.".format(name, len(atomic_inputs), len(atomic_properties)))
            for i, properties in enumerate(atomic_properties):
                for j, inputs in enumerate(atomic_inputs):
                    strategy_number = i*len(properties)+j+1
                    log.info("Strategy variant {}/{}".format(strategy_number, number_of_strategies))
                    strategy_summary = dict()
                    strategy_summary['inputs'] = inputs
                    strategy_summary['properties'] = properties
                    strategy_summary['summary'] = dict()
                    # strategy_summary['summary']['total'], strategy_summary['summary']['interval'], strategy_summary['summary']['symbol'], strategy_summary['raw']
                    strategy_summary['summary']['total'], strategy_summary['summary']['interval'], strategy_summary['summary']['symbol'], strategy_summary['raw'] = back_test_strategy(browser, inputs, properties, symbols, strategy_config, number_of_charts, strategy_number, number_of_strategies)
                    summaries.append(strategy_summary)

        # Inputs have been defined. Run back test for each input with default properties
        elif len(atomic_inputs) > 0:
            log.info("Back testing {} with {} input sets and default property set.".format(name, len(atomic_inputs)))
            for i, inputs in enumerate(atomic_inputs):
                log.info("Strategy variant {}/{}".format(i+1, number_of_strategies))
                strategy_summary = dict()
                strategy_summary['inputs'] = inputs
                strategy_summary['properties'] = []
                strategy_summary['summary'] = dict()
                strategy_summary['summary']['total'], strategy_summary['summary']['interval'], strategy_summary['summary']['symbol'], strategy_summary['raw'] = back_test_strategy(browser, inputs, [], symbols, strategy_config, number_of_charts, i, number_of_strategies)
                summaries.append(strategy_summary)
        # Properties have been defined. Run back test for property with default inputs
        elif len(atomic_properties) > 0:
            log.info("Back testing {} with default input set and {} properties sets.".format(name, len(atomic_properties)))
            for i, properties in enumerate(atomic_properties):
                log.info("Strategy variant {}/{}".format(i+1, number_of_strategies))
                strategy_summary = dict()
                strategy_summary['inputs'] = []
                strategy_summary['properties'] = properties
                strategy_summary['summary'] = dict()
                strategy_summary['summary']['total'], strategy_summary['summary']['interval'], strategy_summary['summary']['symbol'], strategy_summary['raw'] = back_test_strategy(browser, [], properties, symbols, strategy_config, number_of_charts, i, number_of_strategies)
                summaries.append(strategy_summary)
        # Run just one back test with default inputs and properties
        else:
            log.info("Back testing {} with default input set and default property set.".format(name))
            strategy_summary = dict()
            strategy_summary['inputs'] = []
            strategy_summary['properties'] = []
            strategy_summary['summary'] = dict()
            strategy_summary['summary']['total'], strategy_summary['summary']['interval'], strategy_summary['summary']['symbol'], strategy_summary['raw'] = back_test_strategy(browser, [], [], symbols, strategy_config, number_of_charts, 1, 1)
            summaries.append(strategy_summary)

        # close strategy tab
        strategy_tab = find_element(browser, css_selectors['tab_strategy_tester_inactive'], By.CSS_SELECTOR, False, False, 1)
        if isinstance(strategy_tab, WebElement):
            strategy_tab.click()

        return summaries

    except ValueError as e:
        log.exception(e)


def back_test_strategy(browser, inputs, properties, symbols, strategy_config, number_of_charts, strategy_number, number_of_variants):
    global tv_start

    raw = []
    input_locations = dict()
    property_locations = dict()
    interval_averages = dict()
    symbol_averages = dict()
    intervals = []

    duration = 0
    values = dict(
        performance_summary_profit_factor="",
        performance_summary_total_closed_trades="",
        performance_summary_net_profit="",
        performance_summary_net_profit_percentage="",
        performance_summary_percent_profitable="",
        performance_summary_max_drawdown="",
        performance_summary_max_drawdown_percentage="",
        performance_summary_avg_trade="",
        performance_summary_avg_trade_percentage="",
        performance_summary_avg_bars_in_trade="",
    )

    previous_elements = dict(
        performance_summary_profit_factor="",
        performance_summary_total_closed_trades="",
        performance_summary_net_profit="",
        performance_summary_net_profit_percentage="",
        performance_summary_percent_profitable="",
        performance_summary_max_drawdown="",
        performance_summary_max_drawdown_percentage="",
        performance_summary_avg_trade="",
        performance_summary_avg_trade_percentage="",
        performance_summary_avg_bars_in_trade="",
    )

    for i, symbol in enumerate(symbols[0:2]):
        timer_symbol = time.time()
        back_test_strategy_symbol(browser, inputs, properties, symbol, strategy_config, number_of_charts, i == 0, raw, input_locations, property_locations, interval_averages, symbol_averages, intervals, values, previous_elements)
        if i == 0:
            duration += (time.time() - timer_symbol) * (number_of_variants + 1 - strategy_number)
        else:
            duration += (time.time() - timer_symbol) * (len(symbols)-2) * (number_of_variants + 1 - strategy_number)
    log.info("expecting to finish in {}.".format(tools.display_time(duration)))
    for symbol in symbols[2::]:
        first_symbol = refresh_session(browser)
        back_test_strategy_symbol(browser, inputs, properties, symbol, strategy_config, number_of_charts, first_symbol, raw, input_locations, property_locations, interval_averages, symbol_averages, intervals, values, previous_elements)

    # calculate interval averages
    total_average = dict()
    total_average['Net Profit'] = 0
    total_average['Net Profit %'] = 0
    total_average['Closed Trades'] = 0
    total_average['Percent Profitable'] = 0
    total_average['Profit Factor'] = 0
    total_average['Max Drawdown'] = 0
    total_average['Max Drawdown %'] = 0
    total_average['Avg Trade'] = 0
    total_average['Avg Trade %'] = 0
    total_average['Avg # Bars In Trade'] = 0

    for interval in interval_averages:
        counter = max(interval_averages[interval]['Counter'], 1)
        interval_averages[interval]['Net Profit'] = format_number(float(interval_averages[interval]['Net Profit']) / counter)
        interval_averages[interval]['Net Profit %'] = format_number(float(interval_averages[interval]['Net Profit %']) / counter)
        interval_averages[interval]['Closed Trades'] = format_number(float(interval_averages[interval]['Closed Trades']) / counter)
        interval_averages[interval]['Percent Profitable'] = format_number(float(interval_averages[interval]['Percent Profitable']) / counter)
        interval_averages[interval]['Profit Factor'] = format_number(float(interval_averages[interval]['Profit Factor']) / counter)
        interval_averages[interval]['Max Drawdown'] = format_number(float(interval_averages[interval]['Max Drawdown']) / counter)
        interval_averages[interval]['Max Drawdown %'] = format_number(float(interval_averages[interval]['Max Drawdown %']) / counter)
        interval_averages[interval]['Avg Trade'] = format_number(float(interval_averages[interval]['Avg Trade']) / counter)
        interval_averages[interval]['Avg Trade %'] = format_number(float(interval_averages[interval]['Avg Trade %']) / counter)
        interval_averages[interval]['Avg # Bars In Trade'] = format_number(float(interval_averages[interval]['Avg # Bars In Trade']) / counter)
        del interval_averages[interval]['Counter']

        # log.info("{}: {}".format(interval, averages[interval]))

        total_average['Net Profit'] = format_number(float(total_average['Net Profit']) + float(interval_averages[interval]['Net Profit']))
        total_average['Net Profit %'] = format_number(float(total_average['Net Profit %']) + float(interval_averages[interval]['Net Profit %']))
        total_average['Closed Trades'] = format_number(float(total_average['Closed Trades']) + float(interval_averages[interval]['Closed Trades']))
        total_average['Percent Profitable'] = format_number(float(total_average['Percent Profitable']) + float(interval_averages[interval]['Percent Profitable']))
        total_average['Profit Factor'] = format_number(float(total_average['Profit Factor']) + float(interval_averages[interval]['Profit Factor']))
        total_average['Max Drawdown'] = format_number(float(total_average['Max Drawdown']) + float(interval_averages[interval]['Max Drawdown']))
        total_average['Max Drawdown %'] = format_number(float(total_average['Max Drawdown %']) + float(interval_averages[interval]['Max Drawdown %']))
        total_average['Avg Trade'] = format_number(float(total_average['Avg Trade']) + float(interval_averages[interval]['Avg Trade']))
        total_average['Avg Trade %'] = format_number(float(total_average['Avg Trade %']) + float(interval_averages[interval]['Avg Trade %']))
        total_average['Avg # Bars In Trade'] = format_number(float(total_average['Avg # Bars In Trade']) + float(interval_averages[interval]['Avg # Bars In Trade']))

    total_average['Net Profit'] = format_number(float(total_average['Net Profit']) / max(len(interval_averages), 1))
    total_average['Net Profit %'] = format_number(float(total_average['Net Profit %']) / max(len(interval_averages), 1))
    total_average['Closed Trades'] = format_number(float(total_average['Closed Trades']) / max(len(interval_averages), 1))
    total_average['Percent Profitable'] = format_number(float(total_average['Percent Profitable']) / max(len(interval_averages), 1))
    total_average['Profit Factor'] = format_number(float(total_average['Profit Factor']) / max(len(interval_averages), 1))
    total_average['Max Drawdown'] = format_number(float(total_average['Max Drawdown']) / max(len(interval_averages), 1))
    total_average['Max Drawdown %'] = format_number(float(total_average['Max Drawdown %']) / max(len(interval_averages), 1))
    total_average['Avg Trade'] = format_number(float(total_average['Avg Trade']) / max(len(interval_averages), 1))
    total_average['Avg Trade %'] = format_number(float(total_average['Avg Trade %']) / max(len(interval_averages), 1))
    total_average['Avg # Bars In Trade'] = format_number(float(total_average['Avg # Bars In Trade']) / max(len(interval_averages), 1))

    return [total_average, interval_averages, symbol_averages, raw]


def back_test_sort_watchlist(test_runs, sort_by, reverse=True):

    for i, test_run in enumerate(test_runs):
        raw = test_run["raw"]
        interval_averages = test_run["summary"]["interval"]
        symbol_averages = test_run["summary"]["symbol"]
        if sort_by:
            interval_averages_keys = sorted(interval_averages, key=lambda x: interval_averages[x][sort_by], reverse=reverse)
            symbol_averages_keys = sorted(symbol_averages, key=lambda x: symbol_averages[x][sort_by], reverse=reverse)
            raw = sorted(raw, key=lambda x: x[sort_by], reverse=reverse)
        else:
            # fall back to default sorting
            interval_averages_keys = sorted(interval_averages)
            symbol_averages_keys = sorted(symbol_averages)

        interval_averages_sorted = dict()
        for key in interval_averages_keys:
            interval_averages_sorted[key] = interval_averages[key]
        symbol_averages_sorted = dict()
        for key in symbol_averages_keys:
            symbol_averages_sorted[key] = symbol_averages[key]

        test_run["summary"]["interval"] = interval_averages_sorted
        test_run["summary"]["symbol"] = symbol_averages_sorted
        test_run["raw"] = raw

    if sort_by:
        result = sorted(test_runs, key=lambda x: x["summary"]["total"][sort_by], reverse=reverse)
    else:
        result = test_runs
    return result


def back_test_sort(json_data, sort_by, reverse=True):
    # log.info("{} {}".format(sort_by, reverse))
    try:
        for strategy in json_data:
            # log.info("{}: {}".format(strategy, type(json_data[strategy])))
            if isinstance(json_data[strategy], dict):
                for watchlist in json_data[strategy]:
                    if (watchlist not in ["id", "default_inputs", "default_properties"]) and isinstance(json_data[strategy][watchlist], list):
                        json_data[strategy][watchlist] = back_test_sort_watchlist(json_data[strategy][watchlist], sort_by, reverse)
        return json_data
    except Exception as e:
        log.exception(e)


def back_test_strategy_symbol(browser, inputs, properties, symbol, strategy_config, number_of_charts, first_symbol, results, input_locations, property_locations, interval_averages, symbol_averages, intervals, values, previous_elements, tries=0):
    try:
        log.info(symbol)
        if first_symbol:
            open_performance_summary_tab(browser)

        input_symbol = find_element(browser, css_selectors['input_symbol'])
        set_value(browser, input_symbol, symbol)
        input_symbol.send_keys(Keys.ENTER)

        symbol_average = dict()
        symbol_average['Net Profit'] = 0
        symbol_average['Net Profit %'] = 0
        symbol_average['Closed Trades'] = 0
        symbol_average['Percent Profitable'] = 0
        symbol_average['Profit Factor'] = 0
        symbol_average['Max Drawdown'] = 0
        symbol_average['Max Drawdown %'] = 0
        symbol_average['Avg Trade'] = 0
        symbol_average['Avg Trade %'] = 0
        symbol_average['Avg # Bars In Trade'] = 0
        symbol_average['Counter'] = 0

        for chart_index in range(number_of_charts):
            # move to correct chart
            charts = find_elements(browser, "div.chart-container")
            charts[chart_index].click()
            # first time chart setup
            # - set inputs and properties of charts
            # - get interval of chart
            # - create a dict() for each interval and add it to averages
            if first_symbol:
                # log.debug("selecting and formatting strategy for chart {}".format(chart_index + 1))
                # set the strategy if there are inputs or properties defined
                if len(inputs) > 0 or len(properties) > 0:
                    # Select correct strategy on the chart, wait for it to be loaded and get current inputs and properties
                    select_strategy(browser, strategy_config, chart_index)
                    # open the strategy dialog and set the input & property values
                    format_strategy(browser, inputs, properties, input_locations, property_locations)
                elem_interval = find_element(browser, css_selectors['active_chart_interval'])
                interval = repr(elem_interval.get_attribute('innerHTML')).replace(', ', '')
                intervals.append(interval)

                if not (interval in interval_averages):
                    interval_averages[interval] = dict()
                    interval_averages[interval]['Net Profit'] = 0
                    interval_averages[interval]['Net Profit %'] = 0
                    interval_averages[interval]['Closed Trades'] = 0
                    interval_averages[interval]['Percent Profitable'] = 0
                    interval_averages[interval]['Profit Factor'] = 0
                    interval_averages[interval]['Max Drawdown'] = 0
                    interval_averages[interval]['Max Drawdown %'] = 0
                    interval_averages[interval]['Avg Trade'] = 0
                    interval_averages[interval]['Avg Trade %'] = 0
                    interval_averages[interval]['Avg # Bars In Trade'] = 0
                    interval_averages[interval]['Counter'] = 0

            wait_until_indicator_is_loaded(browser, strategy_config['name'], strategy_config['pane_index'])
            interval = intervals[chart_index]

            # take_screenshot(browser, symbol, interval, False, '%Y%m%d_%H%M%S')
            # log.info("previous_element is {}".format(type(previous_element)))

            # Extract results
            over_the_threshold = True
            for i, key in enumerate(values):
                value = get_strategy_statistic(browser, key, previous_elements)
                if isinstance(value, Exception):
                    raise value

                # check if the total closed trades is over the threshold
                if key == 'performance_summary_total_closed_trades' and config.has_option('backtesting', 'threshold') and config.getint('backtesting', 'threshold') > value:
                    log.info("{}: {} data has been excluded due to the number of closed trades ({}) not reaching the threshold ({})".format(symbol, interval, value, config.getint('backtesting', 'threshold')))
                    over_the_threshold = False
                    values[key] = value
                    break
                # Update previous values with the current ones
                values[key] = value

            # previous_element[0] = find_element(browser, css_selectors['performance_summary_profit_factor'])
            # log.info("previous_element = {}".format(repr(previous_element[0])))
            if not over_the_threshold:
                continue
            ############################################################
            # DO NOT ADD INTERACTIONS WITH SELENIUM BELOW THIS COMMENT #
            # Exceptions may give incomplete results. Make sure that   #
            # all Selenium interaction is done above this comment.     #
            ############################################################

            # Save the results
            result = dict()
            result['Symbol'] = symbol
            result['Interval'] = interval.replace("'", "")
            result['Net Profit'] = format_number(float(values['performance_summary_net_profit']), 8)
            result['Net Profit %'] = format_number(float(values['performance_summary_net_profit_percentage']), 8)
            result['Closed Trades'] = format_number(float(values['performance_summary_total_closed_trades']), 8)
            result['Percent Profitable'] = format_number(float(values['performance_summary_percent_profitable']), 8)
            result['Profit Factor'] = format_number(float(values['performance_summary_profit_factor']), 8)
            result['Max Drawdown'] = format_number(float(values['performance_summary_max_drawdown']), 8)
            result['Max Drawdown %'] = format_number(float(values['performance_summary_max_drawdown_percentage']), 8)
            result['Avg Trade'] = format_number(float(values['performance_summary_avg_trade']), 8)
            result['Avg Trade %'] = format_number(float(values['performance_summary_avg_trade_percentage']), 8)
            result['Avg # Bars In Trade'] = format_number(float(values['performance_summary_avg_bars_in_trade']), 8)
            results.append(result)

            # add to averages
            if isinstance(result['Avg # Bars In Trade'], int):
                symbol_average['Net Profit'] = format_number(float(symbol_average['Net Profit']) + float(result['Net Profit']))
                symbol_average['Net Profit %'] = format_number(float(symbol_average['Net Profit %']) + float(result['Net Profit %']))
                symbol_average['Closed Trades'] += int(result['Closed Trades'])
                symbol_average['Percent Profitable'] = format_number(float(symbol_average['Percent Profitable']) + float(result['Percent Profitable']))
                symbol_average['Profit Factor'] = format_number(float(symbol_average['Profit Factor']) + float(result['Profit Factor']))
                symbol_average['Max Drawdown'] = format_number(float(symbol_average['Max Drawdown']) + float(result['Max Drawdown']))
                symbol_average['Max Drawdown %'] = format_number(float(symbol_average['Max Drawdown %']) + float(result['Max Drawdown %']))
                symbol_average['Avg Trade'] = format_number(float(symbol_average['Avg Trade']) + float(result['Avg Trade']))
                symbol_average['Avg Trade %'] = format_number(float(symbol_average['Avg Trade %']) + float(result['Avg Trade %']))
                symbol_average['Avg # Bars In Trade'] += int(result['Avg # Bars In Trade'])
                symbol_average['Counter'] += 1

                interval_averages[interval]['Net Profit'] = format_number(float(interval_averages[interval]['Net Profit']) + float(result['Net Profit']))
                interval_averages[interval]['Net Profit %'] = format_number(float(interval_averages[interval]['Net Profit %']) + float(result['Net Profit %']))
                interval_averages[interval]['Closed Trades'] += int(result['Closed Trades'])
                interval_averages[interval]['Percent Profitable'] = format_number(float(interval_averages[interval]['Percent Profitable']) + float(result['Percent Profitable']))
                interval_averages[interval]['Profit Factor'] = format_number(float(interval_averages[interval]['Profit Factor']) + float(result['Profit Factor']))
                interval_averages[interval]['Max Drawdown'] = format_number(float(interval_averages[interval]['Max Drawdown']) + float(result['Max Drawdown']))
                interval_averages[interval]['Max Drawdown %'] = format_number(float(interval_averages[interval]['Max Drawdown %']) + float(result['Max Drawdown %']))
                interval_averages[interval]['Avg Trade'] = format_number(float(interval_averages[interval]['Avg Trade']) + float(result['Avg Trade']))
                interval_averages[interval]['Avg Trade %'] = format_number(float(interval_averages[interval]['Avg Trade %']) + float(result['Avg Trade %']))
                interval_averages[interval]['Avg # Bars In Trade'] += int(result['Avg # Bars In Trade'])
                interval_averages[interval]['Counter'] += 1

        # calculate symbol averages
        counter = max(symbol_average['Counter'], 1)
        symbol_average['Net Profit'] = format_number(float(symbol_average['Net Profit']) / counter)
        symbol_average['Net Profit %'] = format_number(float(symbol_average['Net Profit %']) / counter)
        symbol_average['Closed Trades'] = format_number(float(symbol_average['Closed Trades']) / counter)
        symbol_average['Percent Profitable'] = format_number(float(symbol_average['Percent Profitable']) / counter)
        symbol_average['Profit Factor'] = format_number(float(symbol_average['Profit Factor']) / counter)
        symbol_average['Max Drawdown'] = format_number(float(symbol_average['Max Drawdown']) / counter)
        symbol_average['Max Drawdown %'] = format_number(float(symbol_average['Max Drawdown %']) / counter)
        symbol_average['Avg Trade'] = format_number(float(symbol_average['Avg Trade']) / counter)
        symbol_average['Avg Trade %'] = format_number(float(symbol_average['Avg Trade %']) / counter)
        symbol_average['Avg # Bars In Trade'] = format_number(float(symbol_average['Avg # Bars In Trade']) / counter)
        del symbol_average['Counter']
        # log.info("{}: {}".format(symbol, symbol_average))
        symbol_averages[symbol] = symbol_average

    except Exception as e:
        retry_back_test_strategy_symbol(browser, inputs, properties, symbol, strategy_config, number_of_charts, first_symbol, results, input_locations, property_locations, interval_averages, symbol_averages, intervals, values, previous_elements, tries, e)


def retry_back_test_strategy_symbol(browser, inputs, properties, symbol, strategy_config, number_of_charts, first_symbol, results, input_locations, property_locations, interval_averages, symbol_averages, intervals, values, previous_elements, tries, e):
    max_tries = config.getint('tradingview', 'create_alert_max_retries')
    if tries < max_tries:
        # log.debug("try {}".format(tries))
        if isinstance(e, InvalidSessionIdException) or isinstance(e, WebDriverException):
            log.exception(e)
            if str(e.msg).lower().find('session') >= 0:
                log.critical("invalid session id - RESTARTING")
                url = browser.current_url
                browser.quit()
                browser = create_browser(RUN_IN_BACKGROUND)
                browser.get(url)
                # Switching to Alert
                close_alerts(browser)
                # Close the watchlist menu if it is open
                if find_element(browser, css_selectors['btn_watchlist'], By.CSS_SELECTOR, False, False, 0.5):
                    wait_and_click(browser, css_selectors['btn_watchlist'])
                first_symbol = True
            else:
                log.exception(e)
                refresh(browser)
        first_symbol = refresh_session(browser) or first_symbol
        if not isinstance(e, StaleElementReferenceException):
            log.exception(e)
            refresh(browser)
        return back_test_strategy_symbol(browser, inputs, properties, symbol, strategy_config, number_of_charts, first_symbol, results, input_locations, property_locations, interval_averages, symbol_averages, intervals, values, previous_elements, tries+1)
    else:
        log.exception(e)
        snapshot(browser, True, False)


def refresh_session(browser):
    global REFRESH_START
    interval_expired = timing.time() - REFRESH_START >= REFRESH_INTERVAL
    if interval_expired:
        refresh(browser)
        REFRESH_START = timing.time()
    return interval_expired


def get_strategy_statistic(browser, key, previous_elements):
    result = 0
    tries = 0
    css = css_selectors[key]
    while tries < config.getint('tradingview', 'create_alert_max_retries'):
        try:
            if isinstance(previous_elements[key], WebElement):
                try:
                    wait_for_element_is_stale(previous_elements[key])
                except TimeoutException as e:
                    log.info(e)
                    pass
                except Exception as e:
                    log.exception(e)

            el = find_element(browser, css, By.CSS_SELECTOR, False, False, 1)
            if not el:
                log.debug("NOT FOUND: {} = {}".format(By.CSS_SELECTOR, css))
                break
            text = repr(el.get_attribute('innerHTML')).replace('\\u2009', '')
            negative = text.find("neg") >= 0

            match = re.search(r"([\d|.]+)", text)
            if match:
                result = match.group(1)
                if negative:
                    result = "-{}".format(result)
            result = fast_real(result)
            if isinstance(result, float):
                result = "{:.10f}".format(result).rstrip('0')
            previous_elements[key] = el
            return result

        except StaleElementReferenceException:
            pass
        except InvalidSessionIdException as e:
            if str(e.msg).lower().find('invalid session id') >= 0:
                log.info("Handling of {} delegated to caller".format(e.msg))
                return e
            else:
                log.exception(e)
                return e
        except WebDriverException as e:
            if str(e.msg).lower().find('invalid session id') >= 0:
                log.info("Handling of {} delegated to caller".format(e.msg))
                return e
            else:
                log.exception(e)
                return e
        except Exception as e:
            # log.debug("{} = {}".format(By.CSS_SELECTOR, css))
            log.exception(e)
            return e
        tries += 1

    return result


def format_strategy(browser, inputs, properties, input_locations, property_locations, retry_number=0):
    try:
        # open dialog
        wait_and_click(browser, css_selectors['btn_strategy_dialog'])
        # click and set inputs
        wait_and_click(browser, css_selectors['indicator_dialog_tab_inputs'])
        set_indicator_dialog_values(browser, inputs, input_locations)
        # click and set properties
        wait_and_click(browser, css_selectors['indicator_dialog_tab_properties'])
        set_indicator_dialog_values(browser, properties, property_locations)
        # click OK
        wait_and_click(browser, css_selectors['btn_indicator_dialog_ok'])
    except StaleElementReferenceException:
        return retry_format_strategy(browser, inputs, properties, input_locations, property_locations, retry_number)
    except Exception as e:
        return e
        # refresh(browser)
        # if not retry_format_strategy(browser, inputs, properties, input_locations, property_locations, retry_number):
        #     log.exception(e)
        #     snapshot(browser, True)
    return True


def set_indicator_dialog_values(browser, inputs, input_locations):
    # get input titles
    cells = find_elements(browser, css_selectors['indicator_dialog_tab_cells'])
    titles = []
    for i, cell in enumerate(cells):
        title = re.sub(r"[\W]", '', cell.text.replace(' ', '_')).lower()
        titles.append(title)

    for key in inputs:
        value = inputs[key]
        index = -1
        for i, title in enumerate(titles):
            if (title == key) or ((not EXACT_CONDITIONS) and title.startswith(key)):
                index = i

        if index >= 0:
            # check first if it is a set of values, e.g. 100 USD
            if isinstance(value, dict):
                for sub_index, sub_key in enumerate(value):
                    sub_value = value[sub_key]
                    set_indicator_dialog_value(browser, input_locations, key, value, index, sub_key, sub_value, sub_index)
            else:
                set_indicator_dialog_value(browser, input_locations, key, value, index)

    return True


def set_indicator_dialog_value(browser, locations, key, value, index, sub_key='', sub_value='', sub_index=-1, retry_number=0):
    css = ''
    if key in locations:
        css = locations[key]
        if isinstance(css, dict):
            if sub_key and sub_key in css:
                css = css[sub_key]
            else:
                css = ''

    try:
        # we need to generate the css
        if not css:
            # check first if it is a set of values, e.g. 100 USD
            if sub_index >= 0:
                # css = css_selectors['indicator_dialog_tab_cell'].format(index + 2) + ' div[class^="inputGroup"] > div:nth-child({})'.format(sub_index + 1)
                css = css_selectors['indicator_dialog_tab_cell'].format(index + 2) + ' > div[class^="inner"] > div > div:nth-child({})'.format(sub_index + 1)
                # check if it is a boolean
                if isinstance(sub_value, bool):
                    css += ' input'
                else:
                    input_css = ' input'
                    element = find_element(browser, css + input_css, By.CSS_SELECTOR, False, False, 1)
                    if element:
                        css += input_css
                    else:
                        css += ' div[class^="selected"]'
                # save the css for future use in this run
                if not (key in locations):
                    locations[key] = dict()
                locations[key][sub_key] = css

            # check if it is a boolean
            elif isinstance(value, bool):
                css = css_selectors['indicator_dialog_tab_cell'].format(index + 1) + ' input'
                locations[key] = css
            else:
                css = css_selectors['indicator_dialog_tab_cell'].format(index + 2)
                input_css = ' input'
                element = find_element(browser, css + input_css, By.CSS_SELECTOR, True, False, 1)
                if element:
                    css += input_css
                else:
                    css += ' div[class^="selected"]'
                # save the css for future use in this run
                locations[key] = css

        if css:
            val = value
            if sub_index >= 0:
                val = sub_value

            element = find_element(browser, css)
            if isinstance(element, WebElement):
                # check if it is an input box
                if element.tag_name == 'input':
                    if element.get_attribute("type") == "checkbox":
                        if is_checkbox_checked(element) != val:
                            wait_and_click(browser, css + " + div")
                    else:
                        clear(element)
                        set_value(browser, element, val, True)

                # assume it is a select box
                else:
                    # click on the select box
                    element.click()
                    # get it's options
                    select_options = find_elements(browser, css_selectors['indicator_dialog_select_options'])
                    for option in select_options:
                        option_value = option.text.strip()
                        if option_value == str(val) or ((not EXACT_CONDITIONS) and option_value.startswith(str(val))):
                            # select the option
                            option.click()
                            break
            else:
                log.error("No element found for {}".format(css))
        else:
            log.error("Unable to generate CSS")
    except StaleElementReferenceException:
        retry_set_indicator_dialog_value(browser, locations, key, value, sub_key, sub_value, sub_index, retry_number)
    except Exception as e:
        return e
    return True


def retry_set_indicator_dialog_value(browser, locations, key, value, sub_key, sub_value, sub_index, retry_number):
    max_retries = config.getint('tradingview', 'create_alert_max_retries')
    if config.has_option('tradingview', 'indicator_values_max_retries'):
        max_retries = config.getint('tradingview', 'indicator_values_max_retries')
    if retry_number < max_retries:
        return set_indicator_dialog_value(browser, locations, key, value, sub_key, sub_value, sub_index, retry_number + 1)
    else:
        return False


def retry_format_strategy(browser, inputs, properties, input_locations, property_locations, retry_number):
    max_retries = config.getint('tradingview', 'create_alert_max_retries')
    if config.has_option('tradingview', 'indicator_values_max_retries'):
        max_retries = config.getint('tradingview', 'indicator_values_max_retries')
    if retry_number < max_retries:
        return format_strategy(browser, inputs, properties, input_locations, property_locations, retry_number + 1)
    else:
        return False


def select_strategy(browser, strategy_config, chart_index, retry_number=0):
    pane_index = -1
    indicator_index = -1
    if 'pane_index' in strategy_config and str(strategy_config['pane_index']).isdigit():
        pane_index = strategy_config['pane_index']
    # use css
    try:
        css = 'div.chart-container.active tr:nth-child({}) .study .pane-legend-title__description'.format((pane_index+1) * 2 - 1)
        studies = find_elements(browser, css)
        for i, study in enumerate(studies):
            study_name = studies[i].text
            log.debug('Found '.format(study_name))
            if study_name.startswith(strategy_config['name']):
                indicator_index = i
                try:
                    if str(study_name).lower().index('loading'):
                        time.sleep(0.1)
                        return retry_select_strategy(browser, strategy_config, chart_index, retry_number)
                    if str(study_name).lower().index('compiling'):
                        time.sleep(0.1)
                        return retry_select_strategy(browser, strategy_config, chart_index, retry_number)
                    if str(study_name).lower().index('error'):
                        time.sleep(0.1)
                        return retry_select_strategy(browser, strategy_config, chart_index, retry_number)
                except ValueError:
                    pass
                break
    except StaleElementReferenceException:
        log.debug('StaleElementReferenceException in studies')
        return retry_select_strategy(browser, strategy_config, chart_index, retry_number)
    except Exception as e:
        return e
        # log.exception(e)
        # refresh(browser)
        # return retry_select_strategy(browser, strategy_config, chart_index, retry_number)
    return indicator_index


def retry_select_strategy(browser, strategy_config, chart_index, retry_number):
    max_retries = config.getint('tradingview', 'create_alert_max_retries') * 10
    if config.has_option('tradingview', 'indicator_values_max_retries'):
        max_retries = config.getint('tradingview', 'indicator_values_max_retries')
    if retry_number < max_retries:
        return select_strategy(browser, strategy_config, chart_index, retry_number + 1)


def generate_atomic_values(items, strategies, depth=0):
    recursive_depth = depth + 1
    result = []
    for item in items:
        if isinstance(items[item], dict):
            sub_results = []
            generate_atomic_values(items[item], sub_results, recursive_depth)
            for sub_result in sub_results:
                tmp = dict(items)
                tmp[item] = sub_result
                tmp_result = generate_atomic_values(tmp, strategies, recursive_depth)
                atomic = True

                for tmp_item in tmp:
                    if isinstance(tmp[tmp_item], dict):
                        for key in tmp[tmp_item]:
                            if isinstance(tmp[tmp_item][key], list):
                                atomic = False
                                break
                        if not atomic:
                            break
                    if isinstance(tmp[tmp_item], list):
                        atomic = False
                        break

                if atomic and tmp not in strategies:
                    strategies.append(tmp)

                result.append(tmp_result)
        elif isinstance(items[item], list):
            for value in items[item]:
                tmp = dict(items)
                tmp[item] = value
                tmp_result = generate_atomic_values(tmp, strategies, recursive_depth)

                atomic = True
                for tmp_item in tmp:
                    if isinstance(tmp[tmp_item], list) or isinstance(tmp[tmp_item], dict):
                        atomic = False
                        break
                if atomic and tmp not in strategies:
                    strategies.append(tmp)
                result.append(tmp_result)
        else:
            result = [items[item]]
    # if all we have at the end of the recursive method call is nothing, then the items is just a single variant, e.g. "{'obv': True, 'macd': False}"
    if depth == 0 and not strategies:
        strategies.append(items)
    return result


def get_config_values(items):
    if isinstance(items, list) or isinstance(items, dict):
        for key in items:
            items[key] = generate_config_values(items[key])
    return items


def generate_config_values(value):
    result = []
    delimeter_range = ' - '
    delimeter_increment = '&'
    increment = None

    # if the value is a list, generate values recursively
    if isinstance(value, list):
        for item in value:
            result.append(generate_config_values(item))
    if isinstance(value, dict):
        for item in value:
            value[item] = generate_config_values(value[item])
        result = value
    elif isinstance(value, str) and value.find(delimeter_range) > 0:
        decimal_places = 0

        if value.find(delimeter_increment) > 0:
            [value, increment] = value.split(delimeter_increment)
            value = value.strip()
            increment = increment.strip()
            decimal_places = increment[::-1].find('.')
            try:
                if decimal_places > 0:
                    increment = float(increment)
                else:
                    increment = int(increment)
            except Exception as e:
                log.exception(e)

        [start, end] = value.split(delimeter_range)
        if not increment:
            increment = 1
            decimal_places = max(start[::-1].find('.'), end[::-1].find('.'))
            if decimal_places > 0:
                increment = '0.'
                for i in range(decimal_places - 1):
                    increment += '0'
                increment += '1'
                increment = float(increment)

        try:
            if start.find('.') >= 0:
                start = float(start)
            else:
                start = int(start)
            if end.find('.') >= 0:
                end = float(end)
            else:
                end = int(end)
        except Exception as e:
            log.exception(e)

        if not (isinstance(start, int) or isinstance(start, float)):
            raise ValueError("Invalid range value: '{}'".format(start))
        if not (isinstance(end, int) or isinstance(end, float)):
            raise ValueError("Invalid range value: '{}'".format(end))
        if not (isinstance(increment, int) or isinstance(increment, float)):
            raise ValueError("Invalid increment value: '{}'".format(increment))

        for number in numpy.arange(start, end, increment):
            if decimal_places > 0:
                result.append(float(round(number, decimal_places)))
            else:
                result.append(int(number))
        result.append(end)
        # if decimal_places > 0:
        #     result.append(float(round(end, decimal_places)))
        # else:
        #     result.append(int(end))
    else:
        result = value
        # log.error("unable to convert {} is of numpy type {} to a python type".format(value, type(value)))
    return result


def wait_until_indicator_is_loaded(browser, indicator_name, pane_index):
    result = False
    tries = 0
    max_tries = 10
    while tries < max_tries:
        try:
            css = 'div.chart-container.active tr:nth-child({}) .study .pane-legend-title__description'.format((pane_index+1) * 2 - 1)
            studies = find_elements(browser, css)
            for i, study in enumerate(studies):
                study_name = studies[i].text
                if study_name.startswith(indicator_name):
                    if str(study_name).lower().find('loading') >= 0 or str(study_name).lower().find('compiling') >= 0 or str(study_name).lower().find('error') >= 0:
                        time.sleep(0.05)
                    else:
                        result = True
                        tries = max_tries
                        break
        except StaleElementReferenceException:
            pass
        except Exception as e:
            return e
        tries += 1

    return result


def summary(total_alerts):
    result = "No alerts or signals set"
    if total_alerts > 0:
        # counted twice for alerts as well as signals
        total_alerts = total_alerts / 2
        elapsed = timing.time() - timing.start
        avg = '%s' % float('%.5g' % (elapsed / total_alerts))
        result = "{} markets screened and {} signals triggered with an average process time of {} seconds per market".format(str(int(math.ceil(total_alerts))), len(triggered_signals), avg)
        # print("{} markets screened and {} signals triggered with an average process time of {} seconds per market.".format(str(int(math.ceil(total_alerts))), len(triggered_signals), avg))
    return result