From 5cb6dd93f68a888b9cb8b14ef2559ad1a20850c7 Mon Sep 17 00:00:00 2001 From: MoonGem <34537029+MoonGem@users.noreply.github.com> Date: Mon, 26 Mar 2018 17:19:20 -0500 Subject: [PATCH 01/27] Create generator.py --- parallel/generator.py | 95 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 parallel/generator.py diff --git a/parallel/generator.py b/parallel/generator.py new file mode 100644 index 000000000..12474a805 --- /dev/null +++ b/parallel/generator.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python3 +import math, sys, os, time, pp, math, re +from io import StringIO +# tuple of all parallel python servers to connect with +ppservers = () +#ppservers = ("10.0.0.1",) + + + + + + + +def backtesting(ind): + er1 = str(ind) + ou1 = str(ind * 1024) + import threading, traceback + from io import StringIO + from freqtrade.main import main, set_loggers + old_stdout = sys.stdout + old_stderr = sys.stderr + ind1 = sys.stdout = StringIO() + ind2 = sys.stderr = StringIO() + dat = threading.Thread(target=main(['backtesting'])) + dat.start() + dat.join() + er1 = ind2.getvalue() + ou1 = ind1.getvalue() + sys.stdout = old_stdout + sys.stderr = old_stderr + return er1, ou1 + +if len(sys.argv) > 1: + ncpus = int(sys.argv[1]) + # Creates jobserver with ncpus workers + job_server = pp.Server(ncpus, ppservers=ppservers) +else: + # Creates jobserver with automatically detected number of workers + job_server = pp.Server(ppservers=ppservers) + +print("Starting pp with", job_server.get_ncpus(), "workers") + + +start_time = time.time() + +# Since jobs are not equal in the execution time, division of the problem +# into a 100 of small subproblems leads to a better load balancing +parts = 128 +rounds = 128 +jobs = [] +current = 0 +for index in range(rounds): + for index in range(parts): + jobs.append(job_server.submit(backtesting, (index,))) + job_server.wait() + for job in jobs: + res = job() + string = str(res) + params = re.search(r'~~~~(.*)~~~~', string) + mfi = re.search(r'MFI Value(.*)XXX', string) + fastd = re.search(r'FASTD Value(.*)XXX', string) + adx = re.search(r'ADX Value(.*)XXX', string) + rsi = re.search(r'RSI Value(.*)XXX', string) + tot = re.search(r'TOTAL(.*)', string).group(1) + total = re.search(r'[-+]?([0-9]*\.[0-9]+|[0-9]+)', tot).group(1) + if total and (float(total) > float(current)): + current = total + print('total better profit paremeters: ') + print(total) + if params: + print(params) + print('~~~~~~') + print('Only enable the above settings, not all settings below are used!') + print('~~~~~~') + if mfi: + print('~~~MFI~~~') + print(mfi.group(1)) + if fastd: + print('~~~FASTD~~~') + print(fastd.group(1)) + if adx: + print('~~~ADX~~~') + print(adx.group(1)) + if rsi: + print('~~~RSI~~~') + print(rsi.group(1)) + + + + + + + jobs = [] +print("Time elapsed: ", time.time() - start_time, "s") +job_server.print_stats() From dfb05827e70521acefbc3c49a2d04fe7c659b7f1 Mon Sep 17 00:00:00 2001 From: MoonGem <34537029+MoonGem@users.noreply.github.com> Date: Mon, 26 Mar 2018 17:19:48 -0500 Subject: [PATCH 02/27] Add files via upload --- parallel/backtesting.py | 298 ++++++++++++++++++++++++++++++ parallel/bittrex.py | 241 ++++++++++++++++++++++++ parallel/config.json | 56 ++++++ parallel/default_strategy.py | 342 +++++++++++++++++++++++++++++++++++ parallel/pp-1.6.4.4.zip | Bin 0 -> 80265 bytes 5 files changed, 937 insertions(+) create mode 100644 parallel/backtesting.py create mode 100644 parallel/bittrex.py create mode 100644 parallel/config.json create mode 100644 parallel/default_strategy.py create mode 100644 parallel/pp-1.6.4.4.zip diff --git a/parallel/backtesting.py b/parallel/backtesting.py new file mode 100644 index 000000000..b3e49e8dd --- /dev/null +++ b/parallel/backtesting.py @@ -0,0 +1,298 @@ +# pragma pylint: disable=missing-docstring, W0212, too-many-arguments + +""" +This module contains the backtesting logic +""" +import logging +from argparse import Namespace +from typing import Dict, Tuple, Any, List, Optional + +import arrow +from pandas import DataFrame, Series +from tabulate import tabulate + +import freqtrade.optimize as optimize +from freqtrade import exchange +from freqtrade.analyze import Analyze +from freqtrade.arguments import Arguments +from freqtrade.configuration import Configuration +from freqtrade.exchange import Bittrex +from freqtrade.misc import file_dump_json +from freqtrade.persistence import Trade + + +logger = logging.getLogger(__name__) + + +class Backtesting(object): + """ + Backtesting class, this class contains all the logic to run a backtest + + To run a backtest: + backtesting = Backtesting(config) + backtesting.start() + """ + def __init__(self, config: Dict[str, Any]) -> None: + self.config = config + self.analyze = None + self.ticker_interval = None + self.tickerdata_to_dataframe = None + self.populate_buy_trend = None + self.populate_sell_trend = None + self._init() + + def _init(self) -> None: + """ + Init objects required for backtesting + :return: None + """ + self.analyze = Analyze(self.config) + self.ticker_interval = self.analyze.strategy.ticker_interval + self.tickerdata_to_dataframe = self.analyze.tickerdata_to_dataframe + self.populate_buy_trend = self.analyze.populate_buy_trend + self.populate_sell_trend = self.analyze.populate_sell_trend + exchange._API = Bittrex({'key': '', 'secret': ''}) + + @staticmethod + def get_timeframe(data: Dict[str, DataFrame]) -> Tuple[arrow.Arrow, arrow.Arrow]: + """ + Get the maximum timeframe for the given backtest data + :param data: dictionary with preprocessed backtesting data + :return: tuple containing min_date, max_date + """ + all_dates = Series([]) + for pair_data in data.values(): + all_dates = all_dates.append(pair_data['date']) + all_dates.sort_values(inplace=True) + return arrow.get(all_dates.iloc[0]), arrow.get(all_dates.iloc[-1]) + + def _generate_text_table(self, data: Dict[str, Dict], results: DataFrame) -> str: + """ + Generates and returns a text table for the given backtest data and the results dataframe + :return: pretty printed table with tabulate as str + """ + stake_currency = self.config.get('stake_currency') + + floatfmt = ('.8f', '.8f', '.8f', '.8f', '.8f') + tabular_data = [] + headers = ['total profit '] + for pair in data: + result = results[results.currency == pair] + tabular_data.append([ + result.profit_BTC.sum(), + ]) + + # Append Total + tabular_data.append([ + 'TOTAL', + len(results.index), + results.profit_percent.mean() * 100.0, + results.profit_BTC.sum(), + results.duration.mean(), + len(results[results.profit_BTC > 0]), + len(results[results.profit_BTC < 0]) + ]) + return tabulate(tabular_data, headers=headers, floatfmt=floatfmt) + + def _get_sell_trade_entry( + self, pair: str, buy_row: DataFrame, + partial_ticker: List, trade_count_lock: Dict, args: Dict) -> Optional[Tuple]: + + stake_amount = args['stake_amount'] + max_open_trades = args.get('max_open_trades', 0) + trade = Trade( + open_rate=buy_row.close, + open_date=buy_row.date, + stake_amount=stake_amount, + amount=stake_amount / buy_row.open, + fee=exchange.get_fee() + ) + + # calculate win/lose forwards from buy point + for sell_row in partial_ticker: + if max_open_trades > 0: + # Increase trade_count_lock for every iteration + trade_count_lock[sell_row.date] = trade_count_lock.get(sell_row.date, 0) + 1 + + buy_signal = sell_row.buy + if self.analyze.should_sell(trade, sell_row.close, sell_row.date, buy_signal, + sell_row.sell): + return \ + sell_row, \ + ( + pair, + trade.calc_profit_percent(rate=sell_row.close), + trade.calc_profit(rate=sell_row.close), + (sell_row.date - buy_row.date).seconds // 60 + ), \ + sell_row.date + return None + + def backtest(self, args: Dict) -> DataFrame: + """ + Implements backtesting functionality + + NOTE: This method is used by Hyperopt at each iteration. Please keep it optimized. + Of course try to not have ugly code. By some accessor are sometime slower than functions. + Avoid, logging on this method + + :param args: a dict containing: + stake_amount: btc amount to use for each trade + processed: a processed dictionary with format {pair, data} + max_open_trades: maximum number of concurrent trades (default: 0, disabled) + realistic: do we try to simulate realistic trades? (default: True) + sell_profit_only: sell if profit only + use_sell_signal: act on sell-signal + :return: DataFrame + """ + headers = ['date', 'buy', 'open', 'close', 'sell'] + processed = args['processed'] + max_open_trades = args.get('max_open_trades', 0) + realistic = args.get('realistic', False) + record = args.get('record', None) + records = [] + trades = [] + trade_count_lock = {} + for pair, pair_data in processed.items(): + pair_data['buy'], pair_data['sell'] = 0, 0 # cleanup from previous run + + ticker_data = self.populate_sell_trend(self.populate_buy_trend(pair_data))[headers] + ticker = [x for x in ticker_data.itertuples()] + + lock_pair_until = None + for index, row in enumerate(ticker): + if row.buy == 0 or row.sell == 1: + continue # skip rows where no buy signal or that would immediately sell off + + if realistic: + if lock_pair_until is not None and row.date <= lock_pair_until: + continue + if max_open_trades > 0: + # Check if max_open_trades has already been reached for the given date + if not trade_count_lock.get(row.date, 0) < max_open_trades: + continue + + trade_count_lock[row.date] = trade_count_lock.get(row.date, 0) + 1 + + ret = self._get_sell_trade_entry(pair, row, ticker[index + 1:], + trade_count_lock, args) + + if ret: + row2, trade_entry, next_date = ret + lock_pair_until = next_date + trades.append(trade_entry) + if record: + # Note, need to be json.dump friendly + # record a tuple of pair, current_profit_percent, + # entry-date, duration + records.append((pair, trade_entry[1], + row.date.strftime('%s'), + row2.date.strftime('%s'), + row.date, trade_entry[3])) + # For now export inside backtest(), maybe change so that backtest() + # returns a tuple like: (dataframe, records, logs, etc) + if record and record.find('trades') >= 0: + logger.info('Dumping backtest results') + file_dump_json('backtest-result.json', records) + labels = ['currency', 'profit_percent', 'profit_BTC', 'duration'] + return DataFrame.from_records(trades, columns=labels) + + def start(self) -> None: + """ + Run a backtesting end-to-end + :return: None + """ + data = {} + pairs = self.config['exchange']['pair_whitelist'] + logger.info('Using stake_currency: %s ...', self.config['stake_currency']) + logger.info('Using stake_amount: %s ...', self.config['stake_amount']) + + if self.config.get('live'): + logger.info('Downloading data for all pairs in whitelist ...') + for pair in pairs: + data[pair] = exchange.get_ticker_history(pair, self.ticker_interval) + else: + logger.info('Using local backtesting data (using whitelist in given config) ...') + + timerange = Arguments.parse_timerange(self.config.get('timerange')) + data = optimize.load_data( + self.config['datadir'], + pairs=pairs, + ticker_interval=self.ticker_interval, + refresh_pairs=self.config.get('refresh_pairs', False), + timerange=timerange + ) + + # Ignore max_open_trades in backtesting, except realistic flag was passed + if self.config.get('realistic_simulation', False): + max_open_trades = self.config['max_open_trades'] + else: + logger.info('Ignoring max_open_trades (realistic_simulation not set) ...') + max_open_trades = 0 + + preprocessed = self.tickerdata_to_dataframe(data) + + # Print timeframe + min_date, max_date = self.get_timeframe(preprocessed) + logger.info( + 'Measuring data from %s up to %s (%s days)..', + min_date.isoformat(), + max_date.isoformat(), + (max_date - min_date).days + ) + + # Execute backtest and print results + sell_profit_only = self.config.get('experimental', {}).get('sell_profit_only', False) + use_sell_signal = self.config.get('experimental', {}).get('use_sell_signal', False) + results = self.backtest( + { + 'stake_amount': self.config.get('stake_amount'), + 'processed': preprocessed, + 'max_open_trades': max_open_trades, + 'realistic': self.config.get('realistic_simulation', False), + 'sell_profit_only': sell_profit_only, + 'use_sell_signal': use_sell_signal, + 'record': self.config.get('export') + } + ) + logger.info( + '\n==================================== ' + 'BACKTESTING REPORT' + ' ====================================\n' + '%s', + self._generate_text_table( + data, + results + ) + ) + + +def setup_configuration(args: Namespace) -> Dict[str, Any]: + """ + Prepare the configuration for the backtesting + :param args: Cli args from Arguments() + :return: Configuration + """ + configuration = Configuration(args) + config = configuration.get_config() + + # Ensure we do not use Exchange credentials + config['exchange']['key'] = '' + config['exchange']['secret'] = '' + + return config + + +def start(args: Namespace) -> None: + """ + Start Backtesting script + :param args: Cli args from Arguments() + :return: None + """ + # Initialize configuration + config = setup_configuration(args) + logger.info('Starting freqtrade in Backtesting mode') + + # Initialize backtesting object + backtesting = Backtesting(config) + backtesting.start() diff --git a/parallel/bittrex.py b/parallel/bittrex.py new file mode 100644 index 000000000..002aa2ddf --- /dev/null +++ b/parallel/bittrex.py @@ -0,0 +1,241 @@ +import logging +from typing import Dict, List, Optional + +from bittrex.bittrex import API_V1_1, API_V2_0 +from bittrex.bittrex import Bittrex as _Bittrex +from requests.exceptions import ContentDecodingError + +from freqtrade import OperationalException +from freqtrade.exchange.interface import Exchange + +logger = logging.getLogger(__name__) + +_API: _Bittrex = None +_API_V2: _Bittrex = None +_EXCHANGE_CONF: dict = {} + + +class Bittrex(Exchange): + """ + Bittrex API wrapper. + """ + # Base URL and API endpoints + BASE_URL: str = 'https://www.bittrex.com' + PAIR_DETAIL_METHOD: str = BASE_URL + '/Market/Index' + + def __init__(self, config: dict) -> None: + global _API, _API_V2, _EXCHANGE_CONF + + _EXCHANGE_CONF.update(config) + _API = _Bittrex( + api_key=_EXCHANGE_CONF['key'], + api_secret=_EXCHANGE_CONF['secret'], + calls_per_second=1, + api_version=API_V1_1, + ) + _API_V2 = _Bittrex( + api_key=_EXCHANGE_CONF['key'], + api_secret=_EXCHANGE_CONF['secret'], + calls_per_second=1, + api_version=API_V2_0, + ) + self.cached_ticker = {} + + @staticmethod + def _validate_response(response) -> None: + """ + Validates the given bittrex response + and raises a ContentDecodingError if a non-fatal issue happened. + """ + temp_error_messages = [ + 'NO_API_RESPONSE', + 'MIN_TRADE_REQUIREMENT_NOT_MET', + ] + if response['message'] in temp_error_messages: + raise ContentDecodingError(response['message']) + + @property + def fee(self) -> float: + # 0.25 %: See https://bittrex.com/fees + return 0.0025 + + def buy(self, pair: str, rate: float, amount: float) -> str: + data = _API.buy_limit(pair.replace('_', '-'), amount, rate) + if not data['success']: + if 'APIKEY_INVALID' in str(data['message']): + print('Api Key...') + else: + Bittrex._validate_response(data) + raise OperationalException('{message} params=({pair}, {rate}, {amount})'.format( + message=data['message'], + pair=pair, + rate=rate, + amount=amount)) + return data['result']['uuid'] + + def sell(self, pair: str, rate: float, amount: float) -> str: + data = _API.sell_limit(pair.replace('_', '-'), amount, rate) + if not data['success']: + if 'APIKEY_INVALID' in str(data['message']): + print('Api Key...') + else: + Bittrex._validate_response(data) + raise OperationalException('{message} params=({pair}, {rate}, {amount})'.format( + message=data['message'], + pair=pair, + rate=rate, + amount=amount)) + return data['result']['uuid'] + + def get_balance(self, currency: str) -> float: + data = _API.get_balance(currency) + if not data['success']: + if 'APIKEY_INVALID' in str(data['message']): + print('Api Key...') + else: + Bittrex._validate_response(data) + raise OperationalException('{message} params=({currency})'.format( + message=data['message'], + currency=currency)) + return float(data['result']['Balance'] or 0.0) + + def get_balances(self): + data = _API.get_balances() + if not data['success']: + if 'APIKEY_INVALID' in str(data['message']): + print('Api Key...') + else: + Bittrex._validate_response(data) + raise OperationalException('{message}'.format(message=data['message'])) + return data['result'] + + def get_ticker(self, pair: str, refresh: Optional[bool] = True) -> dict: + if refresh or pair not in self.cached_ticker.keys(): + data = _API.get_ticker(pair.replace('_', '-')) + if not data['success']: + if 'APIKEY_INVALID' in str(data['message']): + print('Api Key...') + else: + Bittrex._validate_response(data) + raise OperationalException('{message} params=({pair})'.format( + message=data['message'], + pair=pair)) + keys = ['Bid', 'Ask', 'Last'] + if not data.get('result') or\ + not all(key in data.get('result', {}) for key in keys) or\ + not all(data.get('result', {})[key] is not None for key in keys): + raise ContentDecodingError('Invalid response from Bittrex params=({pair})'.format( + pair=pair)) + # Update the pair + self.cached_ticker[pair] = { + 'bid': float(data['result']['Bid']), + 'ask': float(data['result']['Ask']), + 'last': float(data['result']['Last']), + } + return self.cached_ticker[pair] + + def get_ticker_history(self, pair: str, tick_interval: int) -> List[Dict]: + if tick_interval == 1: + interval = 'oneMin' + elif tick_interval == 5: + interval = 'fiveMin' + elif tick_interval == 30: + interval = 'thirtyMin' + elif tick_interval == 60: + interval = 'hour' + elif tick_interval == 1440: + interval = 'Day' + else: + raise ValueError('Unknown tick_interval: {}'.format(tick_interval)) + + data = _API_V2.get_candles(pair.replace('_', '-'), interval) + + # These sanity check are necessary because bittrex cannot keep their API stable. + if not data.get('result'): + raise ContentDecodingError('Invalid response from Bittrex params=({pair})'.format( + pair=pair)) + + for prop in ['C', 'V', 'O', 'H', 'L', 'T']: + for tick in data['result']: + if prop not in tick.keys(): + raise ContentDecodingError('Required property {} not present ' + 'in response params=({})'.format(prop, pair)) + + if not data['success']: + if 'APIKEY_INVALID' in str(data['message']): + print('Api Key...') + else: + Bittrex._validate_response(data) + raise OperationalException('{message} params=({pair})'.format( + message=data['message'], + pair=pair)) + + return data['result'] + + def get_order(self, order_id: str) -> Dict: + data = _API.get_order(order_id) + if not data['success']: + Bittrex._validate_response(data) + raise OperationalException('{message} params=({order_id})'.format( + message=data['message'], + order_id=order_id)) + data = data['result'] + return { + 'id': data['OrderUuid'], + 'type': data['Type'], + 'pair': data['Exchange'].replace('-', '_'), + 'opened': data['Opened'], + 'rate': data['PricePerUnit'], + 'amount': data['Quantity'], + 'remaining': data['QuantityRemaining'], + 'closed': data['Closed'], + } + + def cancel_order(self, order_id: str) -> None: + data = _API.cancel(order_id) + if not data['success']: + if 'APIKEY_INVALID' in str(data['message']): + print('Api Key...') + else: + Bittrex._validate_response(data) + raise OperationalException('{message} params=({order_id})'.format( + message=data['message'], + order_id=order_id)) + + def get_pair_detail_url(self, pair: str) -> str: + return self.PAIR_DETAIL_METHOD + '?MarketName={}'.format(pair.replace('_', '-')) + + def get_markets(self) -> List[str]: + data = _API.get_markets() + if not data['success']: + if 'APIKEY_INVALID' in str(data['message']): + print('Api Key...') + else: + Bittrex._validate_response(data) + raise OperationalException(data['message']) + return [m['MarketName'].replace('-', '_') for m in data['result']] + + def get_market_summaries(self) -> List[Dict]: + data = _API.get_market_summaries() + if not data['success']: + if 'APIKEY_INVALID' in str(data['message']): + print('Api Key...') + else: + Bittrex._validate_response(data) + raise OperationalException(data['message']) + return data['result'] + + def get_wallet_health(self) -> List[Dict]: + data = _API_V2.get_wallet_health() + if not data['success']: + if 'APIKEY_INVALID' in str(data['message']): + print('Api Key...') + else: + Bittrex._validate_response(data) + raise OperationalException(data['message']) + return [{ + 'Currency': entry['Health']['Currency'], + 'IsActive': entry['Health']['IsActive'], + 'LastChecked': entry['Health']['LastChecked'], + 'Notice': entry['Currency'].get('Notice'), + } for entry in data['result']] diff --git a/parallel/config.json b/parallel/config.json new file mode 100644 index 000000000..0bcba4b5e --- /dev/null +++ b/parallel/config.json @@ -0,0 +1,56 @@ +{ + "max_open_trades": 3, + "stake_currency": "BTC", + "stake_amount": 0.00075, + "fiat_display_currency": "USD", + "dry_run": false, + "unfilledtimeout": 600, + "ticker_interval": 5, + "bid_strategy": { + "ask_last_balance": 0.0 + }, + "minimal_roi": { + "35": 0.000, + "30": 0.005, + "25": 0.006, + "20": 0.007, + "15": 0.008, + "10": 0.009, + "5": 0.01, + "0": 0.015 + }, + "stoploss": -0.10, + "exchange": { + "name": "bittrex", + "key": "your_exchange_key", + "secret": "your_exchange_secret", + "pair_whitelist": [ + "BTC_ETH", + "BTC_LTC", + "BTC_ETC", + "BTC_DASH", + "BTC_ZEC", + "BTC_XLM", + "BTC_NXT", + "BTC_POWR", + "BTC_ADA", + "BTC_XMR" + ], + "pair_blacklist": [ + "BTC_DOGE" + ] + }, + "experimental": { + "use_sell_signal": false, + "sell_profit_only": false + }, + "telegram": { + "enabled": true, + "token": "your_telegram_token", + "chat_id": "your_telegram_chat_id" + }, + "initial_state": "running", + "internals": { + "process_throttle_secs": 5 + } +} diff --git a/parallel/default_strategy.py b/parallel/default_strategy.py new file mode 100644 index 000000000..fe642225d --- /dev/null +++ b/parallel/default_strategy.py @@ -0,0 +1,342 @@ + +# --- Do not remove these libs --- +from freqtrade.strategy.interface import IStrategy +from typing import Dict, List +from hyperopt import hp +from functools import reduce +from pandas import DataFrame +# -------------------------------- + +# Add your lib to import here +import talib.abstract as ta +import freqtrade.vendor.qtpylib.indicators as qtpylib +import numpy # noqa + +import random + +# Update this variable if you change the class name +class_name = 'DefaultStrategy' + + +# This class is a sample. Feel free to customize it. + + + + +def Select(): + param = [] + random_items = [] + param.append(str('[' + 'uptrend_long_ema' + '[' + 'enabled' + ']')) + param.append(str('[' + 'macd_below_zero' + '][' + 'enabled' + ']')) + param.append(str('[' + 'uptrend_short_ema' '][' + 'enabled'+ ']')) + param.append(str('[' + 'mfi' '][' + 'enabled'+ ']')) + param.append(str('[' + 'fastd' '][' + 'enabled'+ ']')) + param.append(str('[' + 'adx' '][' + 'enabled'+ ']')) + param.append(str('[' + 'rsi' '][' + 'enabled'+ ']')) + param.append(str('[' + 'over_sar' '][' + 'enabled'+ ']')) + param.append(str('[' + 'green_candle' '][' + 'enabled'+ ']')) + param.append(str('[' + 'uptrend_sma' '][' + 'enabled'+ ']')) + param.append(str('[' + 'closebb' '][' + 'enabled'+ ']')) + param.append(str('[' + 'temabb' '][' + 'enabled'+ ']')) + param.append(str('[' + 'fastdt' '][' + 'enabled'+ ']')) + param.append(str('[' + 'ao' '][' + 'enabled'+ ']')) + param.append(str('[' + 'ema3' '][' + 'enabled'+ ']')) + param.append(str('[' + 'macd' '][' + 'enabled'+ ']')) + param.append(str('[' + 'closesar' '][' + 'enabled'+ ']')) + param.append(str('[' + 'htsine' '][' + 'enabled'+ ']')) + param.append(str('[' + 'has' '][' + 'enabled'+ ']')) + param.append(str('[' + 'plusdi' '][' + 'enabled'+ ']')) + howmany = random.randint(1,20) + random_items = random.choices(population=param, k=howmany) + print('~~~~~~~~~~~~~~~~~~~~~~~~~~~~~') + print('The Parameters Enabled Are As Follows!!!: ' + str(random_items)) + print('~~~~~~~~~~~~~~~~~~~~~~~~~~~~~') + return random_items + + + + +class DefaultStrategy(IStrategy): + """ + This is a test strategy to inspire you. + More information in https://github.com/gcarq/freqtrade/blob/develop/docs/bot-optimization.md + + You can: + - Rename the class name (Do not forget to update class_name) + - Add any methods you want to build your strategy + - Add any lib you need to build your strategy + + You must keep: + - the lib in the section "Do not remove these libs" + - the prototype for the methods: minimal_roi, stoploss, populate_indicators, populate_buy_trend, + populate_sell_trend, hyperopt_space, buy_strategy_generator + """ + + # Minimal ROI designed for the strategy. + # This attribute will be overridden if the config file contains "minimal_roi" + minimal_roi = { + "40": 0.0, + "30": 0.01, + "20": 0.02, + "0": 0.04 + } + + ticker_interval = 5 + + # Optimal stoploss designed for the strategy + # This attribute will be overridden if the config file contains "stoploss" + stoploss = -0.10 + + def populate_indicators(self, dataframe: DataFrame) -> DataFrame: + """ + Adds several different TA indicators to the given DataFrame + + Performance Note: For the best performance be frugal on the number of indicators + you are using. Let uncomment only the indicator you are using in your strategies + or your hyperopt configuration, otherwise you will waste your memory and CPU usage. + """ + + # Momentum Indicator + # ------------------------------------ + + # ADX + dataframe['adx'] = ta.ADX(dataframe) + + + # Awesome oscillator + dataframe['ao'] = qtpylib.awesome_oscillator(dataframe) + + # Commodity Channel Index: values Oversold:<-100, Overbought:>100 + dataframe['cci'] = ta.CCI(dataframe) + + # MACD + macd = ta.MACD(dataframe) + dataframe['macd'] = macd['macd'] + dataframe['macdsignal'] = macd['macdsignal'] + dataframe['macdhist'] = macd['macdhist'] + + # MFI + dataframe['mfi'] = ta.MFI(dataframe) + + # Minus Directional Indicator / Movement + dataframe['minus_dm'] = ta.MINUS_DM(dataframe) + dataframe['minus_di'] = ta.MINUS_DI(dataframe) + + # Plus Directional Indicator / Movement + dataframe['plus_dm'] = ta.PLUS_DM(dataframe) + dataframe['plus_di'] = ta.PLUS_DI(dataframe) + dataframe['minus_di'] = ta.MINUS_DI(dataframe) + + # ROC + dataframe['roc'] = ta.ROC(dataframe) + + # RSI + dataframe['rsi'] = ta.RSI(dataframe) + + # Inverse Fisher transform on RSI, values [-1.0, 1.0] (https://goo.gl/2JGGoy) + rsi = 0.1 * (dataframe['rsi'] - 50) + dataframe['fisher_rsi'] = (numpy.exp(2 * rsi) - 1) / (numpy.exp(2 * rsi) + 1) + + # Inverse Fisher transform on RSI normalized, value [0.0, 100.0] (https://goo.gl/2JGGoy) + dataframe['fisher_rsi_norma'] = 50 * (dataframe['fisher_rsi'] + 1) + + # Stoch + stoch = ta.STOCH(dataframe) + dataframe['slowd'] = stoch['slowd'] + dataframe['slowk'] = stoch['slowk'] + + # Stoch fast + stoch_fast = ta.STOCHF(dataframe) + dataframe['fastd'] = stoch_fast['fastd'] + dataframe['fastk'] = stoch_fast['fastk'] + + # Stoch RSI + stoch_rsi = ta.STOCHRSI(dataframe) + dataframe['fastd_rsi'] = stoch_rsi['fastd'] + dataframe['fastk_rsi'] = stoch_rsi['fastk'] + + + # Overlap Studies + # ------------------------------------ + + """ + # Previous Bollinger bands + # Because ta.BBANDS implementation is broken with small numbers, it actually + # returns middle band for all the three bands. Switch to qtpylib.bollinger_bands + # and use middle band instead. + + dataframe['blower'] = ta.BBANDS(dataframe, nbdevup=2, nbdevdn=2)['lowerband'] + """ + + # Bollinger bands + bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2) + dataframe['bb_lowerband'] = bollinger['lower'] + dataframe['bb_middleband'] = bollinger['mid'] + dataframe['bb_upperband'] = bollinger['upper'] + + + # EMA - Exponential Moving Average + dataframe['ema3'] = ta.EMA(dataframe, timeperiod=3) + dataframe['ema5'] = ta.EMA(dataframe, timeperiod=5) + dataframe['ema10'] = ta.EMA(dataframe, timeperiod=10) + dataframe['ema50'] = ta.EMA(dataframe, timeperiod=50) + dataframe['ema100'] = ta.EMA(dataframe, timeperiod=100) + + # SAR Parabol + dataframe['sar'] = ta.SAR(dataframe) + + # SMA - Simple Moving Average + dataframe['sma'] = ta.SMA(dataframe, timeperiod=40) + + + # TEMA - Triple Exponential Moving Average + dataframe['tema'] = ta.TEMA(dataframe, timeperiod=9) + + # Cycle Indicator + # ------------------------------------ + # Hilbert Transform Indicator - SineWave + hilbert = ta.HT_SINE(dataframe) + dataframe['htsine'] = hilbert['sine'] + dataframe['htleadsine'] = hilbert['leadsine'] + + # Pattern Recognition - Bullish candlestick patterns + # ------------------------------------ + + # Hammer: values [0, 100] + dataframe['CDLHAMMER'] = ta.CDLHAMMER(dataframe) + # Inverted Hammer: values [0, 100] + dataframe['CDLINVERTEDHAMMER'] = ta.CDLINVERTEDHAMMER(dataframe) + # Dragonfly Doji: values [0, 100] + dataframe['CDLDRAGONFLYDOJI'] = ta.CDLDRAGONFLYDOJI(dataframe) + # Piercing Line: values [0, 100] + dataframe['CDLPIERCING'] = ta.CDLPIERCING(dataframe) # values [0, 100] + # Morningstar: values [0, 100] + dataframe['CDLMORNINGSTAR'] = ta.CDLMORNINGSTAR(dataframe) # values [0, 100] + # Three White Soldiers: values [0, 100] + dataframe['CDL3WHITESOLDIERS'] = ta.CDL3WHITESOLDIERS(dataframe) # values [0, 100] + + + # Pattern Recognition - Bearish candlestick patterns + # ------------------------------------ + + # Hanging Man: values [0, 100] + dataframe['CDLHANGINGMAN'] = ta.CDLHANGINGMAN(dataframe) + # Shooting Star: values [0, 100] + dataframe['CDLSHOOTINGSTAR'] = ta.CDLSHOOTINGSTAR(dataframe) + # Gravestone Doji: values [0, 100] + dataframe['CDLGRAVESTONEDOJI'] = ta.CDLGRAVESTONEDOJI(dataframe) + # Dark Cloud Cover: values [0, 100] + dataframe['CDLDARKCLOUDCOVER'] = ta.CDLDARKCLOUDCOVER(dataframe) + # Evening Doji Star: values [0, 100] + dataframe['CDLEVENINGDOJISTAR'] = ta.CDLEVENINGDOJISTAR(dataframe) + # Evening Star: values [0, 100] + dataframe['CDLEVENINGSTAR'] = ta.CDLEVENINGSTAR(dataframe) + + + # Pattern Recognition - Bullish/Bearish candlestick patterns + # ------------------------------------ + + # Three Line Strike: values [0, -100, 100] + dataframe['CDL3LINESTRIKE'] = ta.CDL3LINESTRIKE(dataframe) + # Spinning Top: values [0, -100, 100] + dataframe['CDLSPINNINGTOP'] = ta.CDLSPINNINGTOP(dataframe) # values [0, -100, 100] + # Engulfing: values [0, -100, 100] + dataframe['CDLENGULFING'] = ta.CDLENGULFING(dataframe) # values [0, -100, 100] + # Harami: values [0, -100, 100] + dataframe['CDLHARAMI'] = ta.CDLHARAMI(dataframe) # values [0, -100, 100] + # Three Outside Up/Down: values [0, -100, 100] + dataframe['CDL3OUTSIDE'] = ta.CDL3OUTSIDE(dataframe) # values [0, -100, 100] + # Three Inside Up/Down: values [0, -100, 100] + dataframe['CDL3INSIDE'] = ta.CDL3INSIDE(dataframe) # values [0, -100, 100] + + + # Chart type + # ------------------------------------ + + # Heikinashi stategy + heikinashi = qtpylib.heikinashi(dataframe) + dataframe['ha_open'] = heikinashi['open'] + dataframe['ha_close'] = heikinashi['close'] + dataframe['ha_high'] = heikinashi['high'] + dataframe['ha_low'] = heikinashi['low'] + + + return dataframe + + params = Select() + valm = random.randint(1,100) + print('MFI Value :' + str(valm) + ' XXX') + valfast = random.randint(1,100) + print('FASTD Value :' + str(valfast) + ' XXX') + valadx = random.randint(1,100) + print('ADX Value :' + str(valadx) + ' XXX') + valrsi = random.randint(1,100) + print('RSI Value :' + str(valrsi) + ' XXX') + def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame: + + conditions = [] + # GUARDS AND TRENDS + if 'uptrend_long_ema' in str(self.params): + conditions.append(dataframe['ema50'] > dataframe['ema100']) + if 'macd_below_zero' in str(self.params): + conditions.append(dataframe['macd'] < 0) + if 'uptrend_short_ema' in str(self.params): + conditions.append(dataframe['ema5'] > dataframe['ema10']) + if 'mfi' in str(self.params): + + conditions.append(dataframe['mfi'] < self.valm) + if 'fastd' in str(self.params): + + conditions.append(dataframe['fastd'] < self.valfast) + if 'adx' in str(self.params): + + conditions.append(dataframe['adx'] > self.valadx) + if 'rsi' in str(self.params): + + conditions.append(dataframe['rsi'] < self.valrsi) + if 'over_sar' in str(self.params): + conditions.append(dataframe['close'] > dataframe['sar']) + if 'green_candle' in str(self.params): + conditions.append(dataframe['close'] > dataframe['open']) + if 'uptrend_sma' in str(self.params): + prevsma = dataframe['sma'].shift(1) + conditions.append(dataframe['sma'] > prevsma) + if 'closebb' in str(self.params): + conditions.append(dataframe['close'] < dataframe['bb_lowerband']) + if 'temabb' in str(self.params): + conditions.append(dataframe['tema'] < dataframe['bb_lowerband']) + if 'fastdt' in str(self.params): + conditions.append(qtpylib.crossed_above(dataframe['fastd'], 10.0)) + if 'ao' in str(self.params): + conditions.append(qtpylib.crossed_above(dataframe['ao'], 0.0)) + if 'ema3' in str(self.params): + conditions.append(qtpylib.crossed_above(dataframe['ema3'], dataframe['ema10'])) + if 'macd' in str(self.params): + conditions.append(qtpylib.crossed_above(dataframe['macd'], dataframe['macdsignal'])) + if 'closesar' in str(self.params): + conditions.append(qtpylib.crossed_above(dataframe['close'], dataframe['sar'])) + if 'htsine' in str(self.params): + conditions.append(qtpylib.crossed_above(dataframe['htleadsine'], dataframe['htsine'])) + if 'has' in str(self.params): + conditions.append((qtpylib.crossed_above(dataframe['ha_close'], dataframe['ha_open'])) & (dataframe['ha_low'] == dataframe['ha_open'])) + if 'plusdi' in str(self.params): + conditions.append(qtpylib.crossed_above(dataframe['plus_di'], dataframe['minus_di'])) + + dataframe.loc[ + reduce(lambda x, y: x & y, conditions), + 'buy'] = 1 + + return dataframe + + def populate_sell_trend(self, dataframe: DataFrame) -> DataFrame: + """ + Based on TA indicators, populates the sell signal for the given dataframe + :param dataframe: DataFrame + :return: DataFrame with buy column + """ + dataframe.loc[ + ( + ), + 'sell'] = 1 + return dataframe + diff --git a/parallel/pp-1.6.4.4.zip b/parallel/pp-1.6.4.4.zip new file mode 100644 index 0000000000000000000000000000000000000000..793a49caa4ef68e374e46775f2021e193a767b01 GIT binary patch literal 80265 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 literal 0 HcmV?d00001 From 911a021f635825c64fd9a811f947b63079937572 Mon Sep 17 00:00:00 2001 From: MoonGem <34537029+MoonGem@users.noreply.github.com> Date: Mon, 26 Mar 2018 17:20:34 -0500 Subject: [PATCH 03/27] Update generator.py --- parallel/generator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/parallel/generator.py b/parallel/generator.py index 12474a805..e9234253e 100644 --- a/parallel/generator.py +++ b/parallel/generator.py @@ -56,7 +56,7 @@ for index in range(rounds): for job in jobs: res = job() string = str(res) - params = re.search(r'~~~~(.*)~~~~', string) + params = re.search(r'~~~~(.*)~~~~', string).group(1) mfi = re.search(r'MFI Value(.*)XXX', string) fastd = re.search(r'FASTD Value(.*)XXX', string) adx = re.search(r'ADX Value(.*)XXX', string) From 47ff3c6df1ab17b1cda022e47fcfe0e681805c72 Mon Sep 17 00:00:00 2001 From: MoonGem <34537029+MoonGem@users.noreply.github.com> Date: Mon, 26 Mar 2018 17:30:48 -0500 Subject: [PATCH 04/27] Create README.md --- parallel/README.md | 72 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 parallel/README.md diff --git a/parallel/README.md b/parallel/README.md new file mode 100644 index 000000000..5f9b255bf --- /dev/null +++ b/parallel/README.md @@ -0,0 +1,72 @@ +1. install parallel: + +```unzip pp-1.6.4.4.zip``` + +```cd pp-1.6.4.4``` + +```python3.6 setup.py install``` + +2. Move Generator.py into the parent folder, or your "main freqtrade folder." + +```mv Generator.py ../``` + +3. Move the default strategy over the default strategy in freqtrade/strategies folder. + +4. Move backtesting.py over the backtesting.py in freqtrade/optimize folder. + +5. Optinlally install modded bittrex.py + +6. Install dependencies: + +``` +sudo add-apt-repository ppa:jonathonf/python-3.6 + +sudo apt-get update + +sudo apt-get install python3.6 python3.6-venv python3.6-dev build-essential autoconf libtool pkg-config make wget git + +``` + + +7. Install ta-lib: + +``` +wget http://prdownloads.sourceforge.net/ta-lib/ta-lib-0.4.0-src.tar.gz + +tar xvzf ta-lib-0.4.0-src.tar.gz + +cd ta-lib + +./configure --prefix=/usr + +make + +make install + +cd .. + +rm -rf ./ta-lib* + +``` + +8. Install freqtrade: + +``` +cd ~/freqtrade && pip3.6 install -r requirements.txt && python3.6 setup.py install && pip3.6 install -e . +``` + +9. Run generator.py: + +```python3.6 generator.py``` + +10. Wait for results. + +11. Implement these results into your default_strategy: + +***``` +You will need to read the if statements and populate_buy_signal and populate_sell_signal in this file carefully. + +Once implemented, remove the if statements in the populate_buy_trend. + +```*** + From 0f964762e9e41d75c20fb1493e83dd13d8e7d2fd Mon Sep 17 00:00:00 2001 From: MoonGem <34537029+MoonGem@users.noreply.github.com> Date: Mon, 26 Mar 2018 17:31:17 -0500 Subject: [PATCH 05/27] Update README.md --- parallel/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/parallel/README.md b/parallel/README.md index 5f9b255bf..7ed552ea5 100644 --- a/parallel/README.md +++ b/parallel/README.md @@ -63,10 +63,10 @@ cd ~/freqtrade && pip3.6 install -r requirements.txt && python3.6 setup.py insta 11. Implement these results into your default_strategy: -***``` +*** You will need to read the if statements and populate_buy_signal and populate_sell_signal in this file carefully. Once implemented, remove the if statements in the populate_buy_trend. -```*** +*** From 9fc7bc9c1759f5fd52f38df4d7e17e4ebc615cbb Mon Sep 17 00:00:00 2001 From: MoonGem <34537029+MoonGem@users.noreply.github.com> Date: Mon, 26 Mar 2018 20:23:59 -0500 Subject: [PATCH 06/27] Finished --- parallel/generator.py | 93 +++++++++++++++++++++---------------------- 1 file changed, 46 insertions(+), 47 deletions(-) diff --git a/parallel/generator.py b/parallel/generator.py index e9234253e..d7a2fc14b 100644 --- a/parallel/generator.py +++ b/parallel/generator.py @@ -5,10 +5,14 @@ from io import StringIO ppservers = () #ppservers = ("10.0.0.1",) +#note threads are automatically detected for performance - - - +#number of jobs to append each round +parts = 1280000 +#number of times to loop jobs +rounds = 1280000 +jobs = [] +current = 0 def backtesting(ind): @@ -33,10 +37,10 @@ def backtesting(ind): if len(sys.argv) > 1: ncpus = int(sys.argv[1]) # Creates jobserver with ncpus workers - job_server = pp.Server(ncpus, ppservers=ppservers) + job_server = pp.Server(ncpus, ppservers=ppservers, restart=True) else: # Creates jobserver with automatically detected number of workers - job_server = pp.Server(ppservers=ppservers) + job_server = pp.Server(ppservers=ppservers, restart=True) print("Starting pp with", job_server.get_ncpus(), "workers") @@ -45,51 +49,46 @@ start_time = time.time() # Since jobs are not equal in the execution time, division of the problem # into a 100 of small subproblems leads to a better load balancing -parts = 128 -rounds = 128 -jobs = [] -current = 0 + for index in range(rounds): + print('Please wait... it takes a few minutes to hit the while loop after all jobs are appended for the round.') for index in range(parts): jobs.append(job_server.submit(backtesting, (index,))) - job_server.wait() - for job in jobs: - res = job() - string = str(res) - params = re.search(r'~~~~(.*)~~~~', string).group(1) - mfi = re.search(r'MFI Value(.*)XXX', string) - fastd = re.search(r'FASTD Value(.*)XXX', string) - adx = re.search(r'ADX Value(.*)XXX', string) - rsi = re.search(r'RSI Value(.*)XXX', string) - tot = re.search(r'TOTAL(.*)', string).group(1) - total = re.search(r'[-+]?([0-9]*\.[0-9]+|[0-9]+)', tot).group(1) - if total and (float(total) > float(current)): - current = total - print('total better profit paremeters: ') - print(total) - if params: - print(params) - print('~~~~~~') - print('Only enable the above settings, not all settings below are used!') - print('~~~~~~') - if mfi: - print('~~~MFI~~~') - print(mfi.group(1)) - if fastd: - print('~~~FASTD~~~') - print(fastd.group(1)) - if adx: - print('~~~ADX~~~') - print(adx.group(1)) - if rsi: - print('~~~RSI~~~') - print(rsi.group(1)) + while True: + for job in jobs: + try: + res = job() + string = str(res) + params = re.search(r'~~~~(.*)~~~~', string).group(1) + mfi = re.search(r'MFI Value(.*)XXX', string) + fastd = re.search(r'FASTD Value(.*)XXX', string) + adx = re.search(r'ADX Value(.*)XXX', string) + rsi = re.search(r'RSI Value(.*)XXX', string) + tot = re.search(r'TOTAL(.*)', string).group(1) + total = re.search(r'[-+]?([0-9]*\.[0-9]+|[0-9]+)', tot).group(1) + if total and (float(total) > float(current)): + current = total + print('total better profit paremeters: ') + print(total) + if params: + print(params) + print('~~~~~~') + print('Only enable the above settings, not all settings below are used!') + print('~~~~~~') + if mfi: + print('~~~MFI~~~') + print(mfi.group(1)) + if fastd: + print('~~~FASTD~~~') + print(fastd.group(1)) + if adx: + print('~~~ADX~~~') + print(adx.group(1)) + if rsi: + print('~~~RSI~~~') + print(rsi.group(1)) + except exception as e: + print(e) - - - - - - jobs = [] print("Time elapsed: ", time.time() - start_time, "s") job_server.print_stats() From 01f485064795e4a08388911c5f6d8acc0f401e88 Mon Sep 17 00:00:00 2001 From: MoonGem <34537029+MoonGem@users.noreply.github.com> Date: Mon, 26 Mar 2018 20:28:45 -0500 Subject: [PATCH 07/27] It's alive! --- parallel/generator.py | 95 +++++++++++++++++++++---------------------- 1 file changed, 46 insertions(+), 49 deletions(-) diff --git a/parallel/generator.py b/parallel/generator.py index d7a2fc14b..5171e7e3d 100644 --- a/parallel/generator.py +++ b/parallel/generator.py @@ -5,16 +5,17 @@ from io import StringIO ppservers = () #ppservers = ("10.0.0.1",) -#note threads are automatically detected for performance - -#number of jobs to append each round +# Number of jobs to run parts = 1280000 -#number of times to loop jobs -rounds = 1280000 + + + jobs = [] current = 0 + + def backtesting(ind): er1 = str(ind) ou1 = str(ind * 1024) @@ -37,58 +38,54 @@ def backtesting(ind): if len(sys.argv) > 1: ncpus = int(sys.argv[1]) # Creates jobserver with ncpus workers - job_server = pp.Server(ncpus, ppservers=ppservers, restart=True) + job_server = pp.Server(150, ppservers=ppservers, restart=True) else: # Creates jobserver with automatically detected number of workers - job_server = pp.Server(ppservers=ppservers, restart=True) + job_server = pp.Server(150, ppservers=ppservers, restart=True) print("Starting pp with", job_server.get_ncpus(), "workers") start_time = time.time() -# Since jobs are not equal in the execution time, division of the problem -# into a 100 of small subproblems leads to a better load balancing - -for index in range(rounds): - print('Please wait... it takes a few minutes to hit the while loop after all jobs are appended for the round.') - for index in range(parts): - jobs.append(job_server.submit(backtesting, (index,))) - while True: - for job in jobs: - try: - res = job() - string = str(res) - params = re.search(r'~~~~(.*)~~~~', string).group(1) - mfi = re.search(r'MFI Value(.*)XXX', string) - fastd = re.search(r'FASTD Value(.*)XXX', string) - adx = re.search(r'ADX Value(.*)XXX', string) - rsi = re.search(r'RSI Value(.*)XXX', string) - tot = re.search(r'TOTAL(.*)', string).group(1) - total = re.search(r'[-+]?([0-9]*\.[0-9]+|[0-9]+)', tot).group(1) - if total and (float(total) > float(current)): - current = total - print('total better profit paremeters: ') - print(total) - if params: - print(params) - print('~~~~~~') - print('Only enable the above settings, not all settings below are used!') - print('~~~~~~') - if mfi: - print('~~~MFI~~~') - print(mfi.group(1)) - if fastd: - print('~~~FASTD~~~') - print(fastd.group(1)) - if adx: - print('~~~ADX~~~') - print(adx.group(1)) - if rsi: - print('~~~RSI~~~') - print(rsi.group(1)) - except exception as e: - print(e) +for index in range(parts): + jobs.append(job_server.submit(backtesting, (index,))) + # print(index) +while True: + for job in jobs: + try: + res = job() + string = str(res) + params = re.search(r'~~~~(.*)~~~~', string).group(1) + mfi = re.search(r'MFI Value(.*)XXX', string) + fastd = re.search(r'FASTD Value(.*)XXX', string) + adx = re.search(r'ADX Value(.*)XXX', string) + rsi = re.search(r'RSI Value(.*)XXX', string) + tot = re.search(r'TOTAL(.*)', string).group(1) + total = re.search(r'[-+]?([0-9]*\.[0-9]+|[0-9]+)', tot).group(1) + if total and (float(total) > float(current)): + current = total + print('total better profit paremeters: ') + print(total) + if params: + print(params) + print('~~~~~~') + print('Only enable the above settings, not all settings below are used!') + print('~~~~~~') + if mfi: + print('~~~MFI~~~') + print(mfi.group(1)) + if fastd: + print('~~~FASTD~~~') + print(fastd.group(1)) + if adx: + print('~~~ADX~~~') + print(adx.group(1)) + if rsi: + print('~~~RSI~~~') + print(rsi.group(1)) + except exception as e: + print(e) print("Time elapsed: ", time.time() - start_time, "s") job_server.print_stats() From 6008449a4baacc65f1fc4522c8c0a0e0e3aade6d Mon Sep 17 00:00:00 2001 From: MoonGem <34537029+MoonGem@users.noreply.github.com> Date: Mon, 26 Mar 2018 20:29:56 -0500 Subject: [PATCH 08/27] Update generator.py --- parallel/generator.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/parallel/generator.py b/parallel/generator.py index 5171e7e3d..42fa677ef 100644 --- a/parallel/generator.py +++ b/parallel/generator.py @@ -38,10 +38,10 @@ def backtesting(ind): if len(sys.argv) > 1: ncpus = int(sys.argv[1]) # Creates jobserver with ncpus workers - job_server = pp.Server(150, ppservers=ppservers, restart=True) + job_server = pp.Server(ncpus, ppservers=ppservers, restart=True) else: # Creates jobserver with automatically detected number of workers - job_server = pp.Server(150, ppservers=ppservers, restart=True) + job_server = pp.Server(ppservers=ppservers, restart=True) print("Starting pp with", job_server.get_ncpus(), "workers") From 9034a8e090525df22e023dd3a640c67dc7e05b84 Mon Sep 17 00:00:00 2001 From: MoonGem <34537029+MoonGem@users.noreply.github.com> Date: Mon, 26 Mar 2018 20:39:40 -0500 Subject: [PATCH 09/27] Update generator.py --- parallel/generator.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/parallel/generator.py b/parallel/generator.py index 42fa677ef..57e03e665 100644 --- a/parallel/generator.py +++ b/parallel/generator.py @@ -37,11 +37,11 @@ def backtesting(ind): if len(sys.argv) > 1: ncpus = int(sys.argv[1]) - # Creates jobserver with ncpus workers - job_server = pp.Server(ncpus, ppservers=ppservers, restart=True) + # Creates jobserver with ncpus workers restart=True is not needed + job_server = pp.Server(ncpus, ppservers=ppservers) else: # Creates jobserver with automatically detected number of workers - job_server = pp.Server(ppservers=ppservers, restart=True) + job_server = pp.Server(ppservers=ppservers) print("Starting pp with", job_server.get_ncpus(), "workers") From 9f86cf0cdffc221d06916546a930a542c44ca6f2 Mon Sep 17 00:00:00 2001 From: MoonGem <34537029+MoonGem@users.noreply.github.com> Date: Mon, 26 Mar 2018 21:12:55 -0500 Subject: [PATCH 10/27] Update generator.py --- parallel/generator.py | 62 ++++++++++++++++++++++--------------------- 1 file changed, 32 insertions(+), 30 deletions(-) diff --git a/parallel/generator.py b/parallel/generator.py index 57e03e665..4658edd62 100644 --- a/parallel/generator.py +++ b/parallel/generator.py @@ -37,7 +37,7 @@ def backtesting(ind): if len(sys.argv) > 1: ncpus = int(sys.argv[1]) - # Creates jobserver with ncpus workers restart=True is not needed + # Creates jobserver with ncpus workers job_server = pp.Server(ncpus, ppservers=ppservers) else: # Creates jobserver with automatically detected number of workers @@ -55,37 +55,39 @@ while True: for job in jobs: try: res = job() - string = str(res) - params = re.search(r'~~~~(.*)~~~~', string).group(1) - mfi = re.search(r'MFI Value(.*)XXX', string) - fastd = re.search(r'FASTD Value(.*)XXX', string) - adx = re.search(r'ADX Value(.*)XXX', string) - rsi = re.search(r'RSI Value(.*)XXX', string) - tot = re.search(r'TOTAL(.*)', string).group(1) - total = re.search(r'[-+]?([0-9]*\.[0-9]+|[0-9]+)', tot).group(1) - if total and (float(total) > float(current)): - current = total - print('total better profit paremeters: ') - print(total) - if params: - print(params) - print('~~~~~~') - print('Only enable the above settings, not all settings below are used!') - print('~~~~~~') - if mfi: - print('~~~MFI~~~') - print(mfi.group(1)) - if fastd: - print('~~~FASTD~~~') - print(fastd.group(1)) - if adx: - print('~~~ADX~~~') - print(adx.group(1)) - if rsi: - print('~~~RSI~~~') - print(rsi.group(1)) + if res is not None: + string = str(res) + params = re.search(r'~~~~(.*)~~~~', string).group(1) + mfi = re.search(r'MFI Value(.*)XXX', string) + fastd = re.search(r'FASTD Value(.*)XXX', string) + adx = re.search(r'ADX Value(.*)XXX', string) + rsi = re.search(r'RSI Value(.*)XXX', string) + tot = re.search(r'TOTAL(.*)', string).group(1) + total = re.search(r'[-+]?([0-9]*\.[0-9]+|[0-9]+)', tot).group(1) + if total and (float(total) > float(current)): + current = total + print('total better profit paremeters: ') + print(total) + if params: + print(params) + print('~~~~~~') + print('Only enable the above settings, not all settings below are used!') + print('~~~~~~') + if mfi: + print('~~~MFI~~~') + print(mfi.group(1)) + if fastd: + print('~~~FASTD~~~') + print(fastd.group(1)) + if adx: + print('~~~ADX~~~') + print(adx.group(1)) + if rsi: + print('~~~RSI~~~') + print(rsi.group(1)) except exception as e: print(e) + pass print("Time elapsed: ", time.time() - start_time, "s") job_server.print_stats() From 42dc90ed1d97a6fa2d3c4470030afe16f3562fbe Mon Sep 17 00:00:00 2001 From: MoonGem <34537029+MoonGem@users.noreply.github.com> Date: Mon, 26 Mar 2018 21:18:35 -0500 Subject: [PATCH 11/27] Print stats every best profit --- parallel/generator.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/parallel/generator.py b/parallel/generator.py index 4658edd62..38e4b6cf2 100644 --- a/parallel/generator.py +++ b/parallel/generator.py @@ -85,9 +85,7 @@ while True: if rsi: print('~~~RSI~~~') print(rsi.group(1)) - except exception as e: - print(e) + print("Time elapsed: ", time.time() - start_time, "s") + job_server.print_stats() + except: pass - -print("Time elapsed: ", time.time() - start_time, "s") -job_server.print_stats() From f011ac19ab6900f27e70638de265c1ea26c89bd5 Mon Sep 17 00:00:00 2001 From: MoonGem <34537029+MoonGem@users.noreply.github.com> Date: Mon, 26 Mar 2018 23:28:31 -0500 Subject: [PATCH 12/27] Fixed float regex to include negative --- parallel/generator.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/parallel/generator.py b/parallel/generator.py index 38e4b6cf2..cd8b6da4b 100644 --- a/parallel/generator.py +++ b/parallel/generator.py @@ -62,8 +62,8 @@ while True: fastd = re.search(r'FASTD Value(.*)XXX', string) adx = re.search(r'ADX Value(.*)XXX', string) rsi = re.search(r'RSI Value(.*)XXX', string) - tot = re.search(r'TOTAL(.*)', string).group(1) - total = re.search(r'[-+]?([0-9]*\.[0-9]+|[0-9]+)', tot).group(1) + tot = re.search(r'TOTAL (.*)', string).group(1) + total = float(tot) if total and (float(total) > float(current)): current = total print('total better profit paremeters: ') From ec535fe4f4f4b69d2ef6cf1f7b4bab9ce378950d Mon Sep 17 00:00:00 2001 From: MoonGem <34537029+MoonGem@users.noreply.github.com> Date: Mon, 26 Mar 2018 23:29:22 -0500 Subject: [PATCH 13/27] Updated --- parallel/backtesting.py | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/parallel/backtesting.py b/parallel/backtesting.py index b3e49e8dd..209429b2b 100644 --- a/parallel/backtesting.py +++ b/parallel/backtesting.py @@ -73,24 +73,14 @@ class Backtesting(object): """ stake_currency = self.config.get('stake_currency') - floatfmt = ('.8f', '.8f', '.8f', '.8f', '.8f') + floatfmt = ('.8f', '.8f', '.8f', '.8f', '.1f') tabular_data = [] - headers = ['total profit '] - for pair in data: - result = results[results.currency == pair] - tabular_data.append([ - result.profit_BTC.sum(), - ]) + headers = ['total profit ' + stake_currency] # Append Total tabular_data.append([ 'TOTAL', - len(results.index), - results.profit_percent.mean() * 100.0, results.profit_BTC.sum(), - results.duration.mean(), - len(results[results.profit_BTC > 0]), - len(results[results.profit_BTC < 0]) ]) return tabulate(tabular_data, headers=headers, floatfmt=floatfmt) @@ -255,7 +245,7 @@ class Backtesting(object): 'record': self.config.get('export') } ) - logger.info( + print( '\n==================================== ' 'BACKTESTING REPORT' ' ====================================\n' From 8296ac646dfcb6205f8c01ced016f93e5abae6b4 Mon Sep 17 00:00:00 2001 From: MoonGem <34537029+MoonGem@users.noreply.github.com> Date: Mon, 26 Mar 2018 23:31:46 -0500 Subject: [PATCH 14/27] Update backtesting.py --- parallel/backtesting.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/parallel/backtesting.py b/parallel/backtesting.py index 209429b2b..d45b40796 100644 --- a/parallel/backtesting.py +++ b/parallel/backtesting.py @@ -73,7 +73,7 @@ class Backtesting(object): """ stake_currency = self.config.get('stake_currency') - floatfmt = ('.8f', '.8f', '.8f', '.8f', '.1f') + floatfmt = ('.8f', '.8f', '.8f', '.8f', '.8f') tabular_data = [] headers = ['total profit ' + stake_currency] From 0a42d5c58b97c960cb5e73ed92a8a22669e0c385 Mon Sep 17 00:00:00 2001 From: MoonGem <34537029+MoonGem@users.noreply.github.com> Date: Mon, 26 Mar 2018 23:57:58 -0500 Subject: [PATCH 15/27] Update generator.py --- parallel/generator.py | 67 +++++++++++++++++++++---------------------- 1 file changed, 33 insertions(+), 34 deletions(-) diff --git a/parallel/generator.py b/parallel/generator.py index cd8b6da4b..c6b91cf40 100644 --- a/parallel/generator.py +++ b/parallel/generator.py @@ -6,7 +6,7 @@ ppservers = () #ppservers = ("10.0.0.1",) # Number of jobs to run -parts = 1280000 +parts = 32 @@ -41,7 +41,7 @@ if len(sys.argv) > 1: job_server = pp.Server(ncpus, ppservers=ppservers) else: # Creates jobserver with automatically detected number of workers - job_server = pp.Server(ppservers=ppservers) + job_server = pp.Server(32, ppservers=ppservers) print("Starting pp with", job_server.get_ncpus(), "workers") @@ -55,37 +55,36 @@ while True: for job in jobs: try: res = job() - if res is not None: - string = str(res) - params = re.search(r'~~~~(.*)~~~~', string).group(1) - mfi = re.search(r'MFI Value(.*)XXX', string) - fastd = re.search(r'FASTD Value(.*)XXX', string) - adx = re.search(r'ADX Value(.*)XXX', string) - rsi = re.search(r'RSI Value(.*)XXX', string) - tot = re.search(r'TOTAL (.*)', string).group(1) - total = float(tot) - if total and (float(total) > float(current)): - current = total - print('total better profit paremeters: ') - print(total) - if params: - print(params) - print('~~~~~~') - print('Only enable the above settings, not all settings below are used!') - print('~~~~~~') - if mfi: - print('~~~MFI~~~') - print(mfi.group(1)) - if fastd: - print('~~~FASTD~~~') - print(fastd.group(1)) - if adx: - print('~~~ADX~~~') - print(adx.group(1)) - if rsi: - print('~~~RSI~~~') - print(rsi.group(1)) - print("Time elapsed: ", time.time() - start_time, "s") - job_server.print_stats() + string = str(res) + params = re.search(r'~~~~(.*)~~~~', string).group(1) + mfi = re.search(r'MFI Value(.*)XXX', string) + fastd = re.search(r'FASTD Value(.*)XXX', string) + adx = re.search(r'ADX Value(.*)XXX', string) + rsi = re.search(r'RSI Value(.*)XXX', string) + tot = re.search(r'TOTAL (.*)\\n', string).group(1) + total = float(tot) + if total and (float(total) > float(current)): + current = total + print('total better profit paremeters: ') + print(format(total, '.8f')) + if params: + print(params) + print('~~~~~~') + print('Only enable the above settings, not all settings below are used!') + print('~~~~~~') + if mfi: + print('~~~MFI~~~') + print(mfi.group(1)) + if fastd: + print('~~~FASTD~~~') + print(fastd.group(1)) + if adx: + print('~~~ADX~~~') + print(adx.group(1)) + if rsi: + print('~~~RSI~~~') + print(rsi.group(1)) + print("Time elapsed: ", time.time() - start_time, "s") + job_server.print_stats() except: pass From bd85627bf2e092bd5f02bc42e65ebd3b9be9a7b3 Mon Sep 17 00:00:00 2001 From: MoonGem <34537029+MoonGem@users.noreply.github.com> Date: Mon, 26 Mar 2018 23:59:00 -0500 Subject: [PATCH 16/27] Update generator.py --- parallel/generator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/parallel/generator.py b/parallel/generator.py index c6b91cf40..ccb301f36 100644 --- a/parallel/generator.py +++ b/parallel/generator.py @@ -41,7 +41,7 @@ if len(sys.argv) > 1: job_server = pp.Server(ncpus, ppservers=ppservers) else: # Creates jobserver with automatically detected number of workers - job_server = pp.Server(32, ppservers=ppservers) + job_server = pp.Server(ppservers=ppservers) print("Starting pp with", job_server.get_ncpus(), "workers") From 1e91ab86a8f4b1db100af06a92ac8055c3982a7b Mon Sep 17 00:00:00 2001 From: MoonGem <34537029+MoonGem@users.noreply.github.com> Date: Tue, 27 Mar 2018 00:00:12 -0500 Subject: [PATCH 17/27] Update generator.py --- parallel/generator.py | 1 + 1 file changed, 1 insertion(+) diff --git a/parallel/generator.py b/parallel/generator.py index ccb301f36..83225bb51 100644 --- a/parallel/generator.py +++ b/parallel/generator.py @@ -88,3 +88,4 @@ while True: job_server.print_stats() except: pass + print('Searching... in loop') From 69f4a25886c3061fd7fb6cdc44a769394e2d3f1a Mon Sep 17 00:00:00 2001 From: MoonGem <34537029+MoonGem@users.noreply.github.com> Date: Tue, 27 Mar 2018 00:00:32 -0500 Subject: [PATCH 18/27] Update generator.py --- parallel/generator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/parallel/generator.py b/parallel/generator.py index 83225bb51..9119c5d80 100644 --- a/parallel/generator.py +++ b/parallel/generator.py @@ -6,7 +6,7 @@ ppservers = () #ppservers = ("10.0.0.1",) # Number of jobs to run -parts = 32 +parts = 10000 From 0b5397e77d09c1eec27b950cee00d3f3b55c9ef2 Mon Sep 17 00:00:00 2001 From: MoonGem <34537029+MoonGem@users.noreply.github.com> Date: Tue, 27 Mar 2018 00:01:00 -0500 Subject: [PATCH 19/27] One million searches --- parallel/generator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/parallel/generator.py b/parallel/generator.py index 9119c5d80..6b0e66092 100644 --- a/parallel/generator.py +++ b/parallel/generator.py @@ -6,7 +6,7 @@ ppservers = () #ppservers = ("10.0.0.1",) # Number of jobs to run -parts = 10000 +parts = 1000000 From 195dbf780737410474242759ef8938de9e95fe0c Mon Sep 17 00:00:00 2001 From: MoonGem <34537029+MoonGem@users.noreply.github.com> Date: Tue, 27 Mar 2018 01:08:07 -0500 Subject: [PATCH 20/27] Fixed not looping, hang --- parallel/generator.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/parallel/generator.py b/parallel/generator.py index 6b0e66092..42fde584d 100644 --- a/parallel/generator.py +++ b/parallel/generator.py @@ -47,10 +47,10 @@ print("Starting pp with", job_server.get_ncpus(), "workers") start_time = time.time() - -for index in range(parts): +i = 0 +while parts < i: + i += 1 jobs.append(job_server.submit(backtesting, (index,))) - # print(index) while True: for job in jobs: try: @@ -88,4 +88,5 @@ while True: job_server.print_stats() except: pass - print('Searching... in loop') + print('Searching... in loop... waiting 30s') + time.sleep(30) From 76e54a682a67ec20ef204831d29322683e3e30f8 Mon Sep 17 00:00:00 2001 From: MoonGem <34537029+MoonGem@users.noreply.github.com> Date: Tue, 27 Mar 2018 01:08:41 -0500 Subject: [PATCH 21/27] Update generator.py --- parallel/generator.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/parallel/generator.py b/parallel/generator.py index 42fde584d..feb435ee6 100644 --- a/parallel/generator.py +++ b/parallel/generator.py @@ -8,14 +8,9 @@ ppservers = () # Number of jobs to run parts = 1000000 - - jobs = [] current = 0 - - - def backtesting(ind): er1 = str(ind) ou1 = str(ind * 1024) From 761fb92d2d257bce8740051460bfbf9a1be481dd Mon Sep 17 00:00:00 2001 From: MoonGem <34537029+MoonGem@users.noreply.github.com> Date: Tue, 27 Mar 2018 01:19:00 -0500 Subject: [PATCH 22/27] Fixed loop exiting --- parallel/generator.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/parallel/generator.py b/parallel/generator.py index feb435ee6..6fd26608a 100644 --- a/parallel/generator.py +++ b/parallel/generator.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 import math, sys, os, time, pp, math, re from io import StringIO + # tuple of all parallel python servers to connect with ppservers = () #ppservers = ("10.0.0.1",) @@ -8,9 +9,11 @@ ppservers = () # Number of jobs to run parts = 1000000 + jobs = [] current = 0 + def backtesting(ind): er1 = str(ind) ou1 = str(ind * 1024) @@ -42,10 +45,10 @@ print("Starting pp with", job_server.get_ncpus(), "workers") start_time = time.time() -i = 0 -while parts < i: - i += 1 +index = 1 +while parts > index: jobs.append(job_server.submit(backtesting, (index,))) + index += 1 while True: for job in jobs: try: From 48680feb44708c0f36cb04c24b47732c04978567 Mon Sep 17 00:00:00 2001 From: MoonGem <34537029+MoonGem@users.noreply.github.com> Date: Tue, 27 Mar 2018 01:32:43 -0500 Subject: [PATCH 23/27] Added informational output. Pretty much complete. --- parallel/generator.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/parallel/generator.py b/parallel/generator.py index 6fd26608a..3a9ad8ee9 100644 --- a/parallel/generator.py +++ b/parallel/generator.py @@ -46,10 +46,14 @@ print("Starting pp with", job_server.get_ncpus(), "workers") start_time = time.time() index = 1 + +print('Please wait... sending jobs to server') while parts > index: jobs.append(job_server.submit(backtesting, (index,))) index += 1 while True: + print('Searching... in loop... waiting 15s') + time.sleep(15) for job in jobs: try: res = job() @@ -86,5 +90,3 @@ while True: job_server.print_stats() except: pass - print('Searching... in loop... waiting 30s') - time.sleep(30) From c81d56f9d9d6ce732d5d731286c08113af7c69a9 Mon Sep 17 00:00:00 2001 From: MoonGem <34537029+MoonGem@users.noreply.github.com> Date: Tue, 27 Mar 2018 01:38:52 -0500 Subject: [PATCH 24/27] Update generator.py --- parallel/generator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/parallel/generator.py b/parallel/generator.py index 3a9ad8ee9..614d7e455 100644 --- a/parallel/generator.py +++ b/parallel/generator.py @@ -72,7 +72,7 @@ while True: if params: print(params) print('~~~~~~') - print('Only enable the above settings, not all settings below are used!') + print('Only enable the above settings! Not all below!') print('~~~~~~') if mfi: print('~~~MFI~~~') From 32210e83f73974eec0d6494475af1c9bb995c1d1 Mon Sep 17 00:00:00 2001 From: MoonGem <34537029+MoonGem@users.noreply.github.com> Date: Tue, 27 Mar 2018 01:42:12 -0500 Subject: [PATCH 25/27] PEP8 Compliant --- parallel/generator.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/parallel/generator.py b/parallel/generator.py index 614d7e455..ea1545bcc 100644 --- a/parallel/generator.py +++ b/parallel/generator.py @@ -1,10 +1,14 @@ #!/usr/bin/env python3 -import math, sys, os, time, pp, math, re -from io import StringIO - +import math +import sys +import os +import time +import pp +import math +import re # tuple of all parallel python servers to connect with ppservers = () -#ppservers = ("10.0.0.1",) +# ppservers = ("10.0.0.1",) # Number of jobs to run parts = 1000000 @@ -17,7 +21,7 @@ current = 0 def backtesting(ind): er1 = str(ind) ou1 = str(ind * 1024) - import threading, traceback + import threading from io import StringIO from freqtrade.main import main, set_loggers old_stdout = sys.stdout From 178b40a745ec640459cb520654892b6c95c6c6a8 Mon Sep 17 00:00:00 2001 From: MoonGem <34537029+MoonGem@users.noreply.github.com> Date: Tue, 27 Mar 2018 01:45:57 -0500 Subject: [PATCH 26/27] Update README.md --- parallel/README.md | 78 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/parallel/README.md b/parallel/README.md index 7ed552ea5..0602116e1 100644 --- a/parallel/README.md +++ b/parallel/README.md @@ -70,3 +70,81 @@ Once implemented, remove the if statements in the populate_buy_trend. *** +For ease of use, here is an example: + +The if statements that run the random generator are: + +``` + def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame: + + conditions = [] + # GUARDS AND TRENDS + if 'uptrend_long_ema' in str(self.params): + conditions.append(dataframe['ema50'] > dataframe['ema100']) + if 'macd_below_zero' in str(self.params): + conditions.append(dataframe['macd'] < 0) + if 'uptrend_short_ema' in str(self.params): + conditions.append(dataframe['ema5'] > dataframe['ema10']) + if 'mfi' in str(self.params): + + conditions.append(dataframe['mfi'] < self.valm) + if 'fastd' in str(self.params): + + conditions.append(dataframe['fastd'] < self.valfast) + if 'adx' in str(self.params): + + conditions.append(dataframe['adx'] > self.valadx) + if 'rsi' in str(self.params): + + conditions.append(dataframe['rsi'] < self.valrsi) + if 'over_sar' in str(self.params): + conditions.append(dataframe['close'] > dataframe['sar']) + if 'green_candle' in str(self.params): + conditions.append(dataframe['close'] > dataframe['open']) + if 'uptrend_sma' in str(self.params): + prevsma = dataframe['sma'].shift(1) + conditions.append(dataframe['sma'] > prevsma) + if 'closebb' in str(self.params): + conditions.append(dataframe['close'] < dataframe['bb_lowerband']) + if 'temabb' in str(self.params): + conditions.append(dataframe['tema'] < dataframe['bb_lowerband']) + if 'fastdt' in str(self.params): + conditions.append(qtpylib.crossed_above(dataframe['fastd'], 10.0)) + if 'ao' in str(self.params): + conditions.append(qtpylib.crossed_above(dataframe['ao'], 0.0)) + if 'ema3' in str(self.params): + conditions.append(qtpylib.crossed_above(dataframe['ema3'], dataframe['ema10'])) + if 'macd' in str(self.params): + conditions.append(qtpylib.crossed_above(dataframe['macd'], dataframe['macdsignal'])) + if 'closesar' in str(self.params): + conditions.append(qtpylib.crossed_above(dataframe['close'], dataframe['sar'])) + if 'htsine' in str(self.params): + conditions.append(qtpylib.crossed_above(dataframe['htleadsine'], dataframe['htsine'])) + if 'has' in str(self.params): + conditions.append((qtpylib.crossed_above(dataframe['ha_close'], dataframe['ha_open'])) & (dataframe['ha_low'] == dataframe['ha_open'])) + if 'plusdi' in str(self.params): + conditions.append(qtpylib.crossed_above(dataframe['plus_di'], dataframe['minus_di'])) + + dataframe.loc[ + reduce(lambda x, y: x & y, conditions), + 'buy'] = 1 + + return dataframe +``` + + +So of you get MFI as a option runnning generator.py, and it's option in output is 91, look at the if statements above at mfi, the populate_buy_trend will now look like this: + + +``` + def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame: + dataframe.loc[ + ( + (dataframe['mfi'] < 91) + ), + 'buy'] = 1 + + return dataframe +``` + +It's as simple as that. From 506b11162d3e6332e0214a5aa7c3ba19ccf4b48b Mon Sep 17 00:00:00 2001 From: MoonGem <34537029+MoonGem@users.noreply.github.com> Date: Tue, 27 Mar 2018 04:06:51 -0500 Subject: [PATCH 27/27] Update generator.py --- parallel/generator.py | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/parallel/generator.py b/parallel/generator.py index ea1545bcc..850ec3f7c 100644 --- a/parallel/generator.py +++ b/parallel/generator.py @@ -11,8 +11,8 @@ ppservers = () # ppservers = ("10.0.0.1",) # Number of jobs to run -parts = 1000000 - +work = 32 +cycles = 100 jobs = [] current = 0 @@ -50,14 +50,15 @@ print("Starting pp with", job_server.get_ncpus(), "workers") start_time = time.time() index = 1 - +indey = 1 print('Please wait... sending jobs to server') -while parts > index: - jobs.append(job_server.submit(backtesting, (index,))) - index += 1 -while True: - print('Searching... in loop... waiting 15s') - time.sleep(15) +while cycles > index: + cycles += 1 + while work > indey: + jobs.append(job_server.submit(backtesting, (index,))) + indey += 1 + job_server.wait() + print('Searching this cycle....') for job in jobs: try: res = job() @@ -94,3 +95,8 @@ while True: job_server.print_stats() except: pass + jobs = [] + indey = 1 +print('DONE') +print("Time elapsed: ", time.time() - start_time, "s") +job_server.print_stats()