def load_candles( start_date_str: str, finish_date_str: str) -> Dict[str, Dict[str, Union[str, np.ndarray]]]: start_date = jh.date_to_timestamp(start_date_str) finish_date = jh.date_to_timestamp(finish_date_str) - 60000 # validate if start_date == finish_date: raise ValueError('start_date and finish_date cannot be the same.') if start_date > finish_date: raise ValueError('start_date cannot be bigger than finish_date.') if finish_date > arrow.utcnow().int_timestamp * 1000: raise ValueError("Can't load candle data from the future!") # load and add required warm-up candles for backtest if jh.is_backtesting(): for c in config['app']['considering_candles']: required_candles.inject_required_candles_to_store( required_candles.load_required_candles(c[0], c[1], start_date_str, finish_date_str), c[0], c[1]) # download candles for the duration of the backtest candles = {} for c in config['app']['considering_candles']: exchange, symbol = c[0], c[1] key = jh.key(exchange, symbol) cache_key = '{}-{}-'.format(start_date_str, finish_date_str) + key cached_value = cache.get_value(cache_key) # if cache exists if cached_value: candles_tuple = cached_value # not cached, get and cache for later calls in the next 5 minutes else: # fetch from database candles_tuple = Candle.select( Candle.timestamp, Candle.open, Candle.close, Candle.high, Candle.low, Candle.volume).where( Candle.timestamp.between(start_date, finish_date), Candle.exchange == exchange, Candle.symbol == symbol).order_by( Candle.timestamp.asc()).tuples() # validate that there are enough candles for selected period required_candles_count = (finish_date - start_date) / 60_000 if len(candles_tuple) == 0 or candles_tuple[-1][ 0] != finish_date or candles_tuple[0][0] != start_date: raise exceptions.CandleNotFoundInDatabase( 'Not enough candles for {}. Try running "jesse import-candles"' .format(symbol)) elif len(candles_tuple) != required_candles_count + 1: raise exceptions.CandleNotFoundInDatabase( 'There are missing candles between {} => {}'.format( start_date_str, finish_date_str)) # cache it for near future calls cache.set_value(cache_key, tuple(candles_tuple), expire_seconds=60 * 60 * 24 * 7) candles[key] = { 'exchange': exchange, 'symbol': symbol, 'candles': np.array(candles_tuple) } return candles
def fitness(self, dna) -> tuple: hp = jh.dna_to_hp(self.strategy_hp, dna) # init candle store store.candles.init_storage(5000) # inject required TRAINING candles to the candle store for num, c in enumerate(config['app']['considering_candles']): required_candles.inject_required_candles_to_store( self.training_initial_candles[num], c[0], c[1]) # run backtest simulation simulator(self.training_candles, hp) log = '' # TODO: some of these have to be dynamic based on how many days it's trading for like for example "total" # I'm guessing we should accept "optimal" total from command line if store.completed_trades.count > 5: training_data = stats.trades(store.completed_trades.trades, store.app.daily_balance) total_effect_rate = log10(training_data['total']) / log10( self.optimal_total) if total_effect_rate > 1: total_effect_rate = 1 win_rate = training_data['win_rate'] ratio_config = jh.get_config('env.optimization.ratio', 'sharpe') if ratio_config == 'sharpe': ratio = training_data['sharpe_ratio'] elif ratio_config == 'calmar': ratio = training_data['calmar_ratio'] elif ratio_config == 'sortiono': ratio = training_data['sortino_ratio'] elif ratio_config == 'omega': ratio = training_data['omega_ratio'] else: raise ValueError( 'The entered ratio configuration `{}` for the optimization is unknown. Choose between sharpe, calmar, sortino and omega.' .format(ratio_config)) if ratio < 0: score = 0.0001 # reset store store.reset() return score, log ratio_normalized = jh.normalize(ratio, -.5, 4) # log for debugging/monitoring log = 'win-rate: {}%, total: {}, PNL: {}%'.format( int(win_rate * 100), training_data['total'], round(training_data['net_profit_percentage'], 2), ) score = total_effect_rate * ratio_normalized # perform backtest with testing data. this is using data # model hasn't trained for. if it works well, there is # high change it will do good with future data too. store.reset() store.candles.init_storage(5000) # inject required TESTING candles to the candle store for num, c in enumerate(config['app']['considering_candles']): required_candles.inject_required_candles_to_store( self.testing_initial_candles[num], c[0], c[1]) # run backtest simulation simulator(self.testing_candles, hp) testing_data = stats.trades(store.completed_trades.trades, store.app.daily_balance) # log for debugging/monitoring log += ' || ' if store.completed_trades.count > 0: log += 'win-rate: {}%, total: {}, PNL: {}%'.format( int(testing_data['win_rate'] * 100), testing_data['total'], round(testing_data['net_profit_percentage'], 2), ) if testing_data['net_profit_percentage'] > 0 and training_data[ 'net_profit_percentage'] > 0: log = jh.style(log, 'bold') else: log += 'win-rate: -, total: -, PNL%: -' else: score = 0.0001 # reset store store.reset() return score, log
def fitness(self, dna: str) -> tuple: hp = jh.dna_to_hp(self.strategy_hp, dna) # init candle store store.candles.init_storage(5000) # inject required TRAINING candles to the candle store for num, c in enumerate(config['app']['considering_candles']): required_candles.inject_required_candles_to_store( self.training_initial_candles[num], c[0], c[1] ) # run backtest simulation simulator(self.training_candles, hp) training_data = {'win_rate': None, 'total': None, 'net_profit_percentage': None} testing_data = {'win_rate': None, 'total': None, 'net_profit_percentage': None} # TODO: some of these have to be dynamic based on how many days it's trading for like for example "total" # I'm guessing we should accept "optimal" total from command line if store.completed_trades.count > 5: training_data = stats.trades(store.completed_trades.trades, store.app.daily_balance) total_effect_rate = log10(training_data['total']) / log10(self.optimal_total) total_effect_rate = min(total_effect_rate, 1) ratio_config = jh.get_config('env.optimization.ratio', 'sharpe') if ratio_config == 'sharpe': ratio = training_data['sharpe_ratio'] ratio_normalized = jh.normalize(ratio, -.5, 5) elif ratio_config == 'calmar': ratio = training_data['calmar_ratio'] ratio_normalized = jh.normalize(ratio, -.5, 30) elif ratio_config == 'sortino': ratio = training_data['sortino_ratio'] ratio_normalized = jh.normalize(ratio, -.5, 15) elif ratio_config == 'omega': ratio = training_data['omega_ratio'] ratio_normalized = jh.normalize(ratio, -.5, 5) elif ratio_config == 'serenity': ratio = training_data['serenity_index'] ratio_normalized = jh.normalize(ratio, -.5, 15) elif ratio_config == 'smart sharpe': ratio = training_data['smart_sharpe'] ratio_normalized = jh.normalize(ratio, -.5, 5) elif ratio_config == 'smart sortino': ratio = training_data['smart_sortino'] ratio_normalized = jh.normalize(ratio, -.5, 15) else: raise ValueError( f'The entered ratio configuration `{ratio_config}` for the optimization is unknown. Choose between sharpe, calmar, sortino, serenity, smart shapre, smart sortino and omega.') if ratio < 0: score = 0.0001 # reset store store.reset() return score, training_data, testing_data score = total_effect_rate * ratio_normalized # perform backtest with testing data. this is using data # model hasn't trained for. if it works well, there is # high change it will do good with future data too. store.reset() store.candles.init_storage(5000) # inject required TESTING candles to the candle store for num, c in enumerate(config['app']['considering_candles']): required_candles.inject_required_candles_to_store( self.testing_initial_candles[num], c[0], c[1] ) # run backtest simulation simulator(self.testing_candles, hp) # log for debugging/monitoring if store.completed_trades.count > 0: testing_data = stats.trades(store.completed_trades.trades, store.app.daily_balance) else: score = 0.0001 # reset store store.reset() return score, training_data, testing_data
def fitness(self, dna) -> tuple: hp = jh.dna_to_hp(self.strategy_hp, dna) # init candle store store.candles.init_storage(5000) # inject required TRAINING candles to the candle store required_candles.inject_required_candles_to_store( self.required_initial_training_candles, self.exchange, self.symbol) # run backtest simulation simulator(self.training_candles, hp) log = '' # TODO: some of these have to be dynamic based on how many days it's trading for like for example "total" # I'm guessing we should accept "optimal" total from command line if store.completed_trades.count > 5: training_data = stats.trades(store.completed_trades.trades) optimal_expected_total = 100 total = jh.normalize(training_data['total'], 0, 200) total_effect_rate = log10( training_data['total']) / log10(optimal_expected_total) win_rate = training_data['win_rate'] # log for debugging/monitoring log = 'win_rate:[{}-{}], total:[{}-{}], PNL%:[{}], TER:[{}]'.format( round(win_rate, 2), round(training_data['win_rate'], 2), round(total, 2), training_data['total'], round(training_data['pnl_percentage'], 2), round(total_effect_rate, 3)) # the fitness score score = win_rate * total_effect_rate # perform backtest with testing data. this is using data # model hasn't trained for. if it works well, there is # high change it will do good with future data too. store.reset() store.candles.init_storage(5000) # inject required TESTING candles to the candle store required_candles.inject_required_candles_to_store( self.required_initial_testing_candles, self.exchange, self.symbol) # run backtest simulation simulator(self.testing_candles, hp) testing_data = stats.trades(store.completed_trades.trades) # log for debugging/monitoring log += ' | ' log += 'win_rate:[{}], total:[{}], PNL%:[{}]'.format( round(testing_data['win_rate'], 2), testing_data['total'], round(testing_data['pnl_percentage'], 2), ) if testing_data['pnl_percentage'] > 0 and training_data[ 'pnl_percentage'] > 0: log = jh.style(log, 'bold') else: score = 0.0001 # reset store store.reset() return score, log
def _isolated_backtest(config: dict, routes: List[Dict[str, str]], extra_routes: List[Dict[str, str]], candles: dict, run_silently: bool = True, hyperparameters: dict = None) -> dict: from jesse.services.validators import validate_routes from jesse.modes.backtest_mode import simulator from jesse.config import config as jesse_config, reset_config from jesse.routes import router from jesse.store import store from jesse.config import set_config from jesse.services import metrics from jesse.services import required_candles import jesse.helpers as jh jesse_config['app']['trading_mode'] = 'backtest' # inject (formatted) configuration values set_config(_format_config(config)) # set routes router.initiate(routes, extra_routes) # register_custom_exception_handler() validate_routes(router) # TODO: further validate routes and allow only one exchange # TODO: validate the name of the exchange in the config and the route? or maybe to make sure it's a supported exchange # initiate candle store store.candles.init_storage(5000) # divide candles into warm_up_candles and trading_candles and then inject warm_up_candles max_timeframe = jh.max_timeframe( jesse_config['app']['considering_timeframes']) warm_up_num = config['warm_up_candles'] * jh.timeframe_to_one_minutes( max_timeframe) trading_candles = candles if warm_up_num != 0: for c in jesse_config['app']['considering_candles']: key = jh.key(c[0], c[1]) # update trading_candles trading_candles[key]['candles'] = candles[key]['candles'][ warm_up_num:] # inject warm-up candles required_candles.inject_required_candles_to_store( candles[key]['candles'][:warm_up_num], c[0], c[1]) # run backtest simulation simulator(trading_candles, run_silently, hyperparameters) result = { 'metrics': { 'total': 0, 'win_rate': 0, 'net_profit_percentage': 0 }, 'charts': None, 'logs': None, } if store.completed_trades.count > 0: # add metrics result['metrics'] = metrics.trades(store.completed_trades.trades, store.app.daily_balance) # add charts result['charts'] = charts.portfolio_vs_asset_returns() # add logs result['logs'] = store.logs.info # reset store and config so rerunning would be flawlessly possible reset_config() store.reset() return result