def cancel(self, silent=False) -> None: if self.is_canceled or self.is_executed: return self.canceled_at = jh.now_to_timestamp() self.status = order_statuses.CANCELED if jh.is_live(): self.save() if not silent: txt = f'CANCELED order: {self.symbol}, {self.type}, {self.side}, {self.qty}' if self.price: txt += f', ${round(self.price, 2)}' if jh.is_debuggable('order_cancellation'): logger.info(txt) if jh.is_live(): self.broadcast() if config['env']['notifications']['events'][ 'cancelled_orders']: notify(txt) # handle exchange balance e = selectors.get_exchange(self.exchange) e.on_order_cancellation(self)
def execute(self) -> None: if self.is_canceled or self.is_executed: return self.executed_at = jh.now_to_timestamp() self.status = order_statuses.EXECUTED # log if jh.is_debuggable('order_execution'): logger.info( 'EXECUTED order: {}, {}, {}, {}, ${}'.format( self.symbol, self.type, self.side, self.qty, round(self.price, 2) ) ) # notify if jh.is_live() and config['env']['notifications']['events']['executed_orders']: notify( 'EXECUTED order: {}, {}, {}, {}, {}'.format( self.symbol, self.type, self.side, self.qty, round(self.price, 2) ) ) p = selectors.get_position(self.exchange, self.symbol) if p: p._on_executed_order(self) # handle exchange balance for ordered asset e = selectors.get_exchange(self.exchange) e.on_order_execution(self)
def __init__(self, attributes=None, **kwargs)-> None: Model.__init__(self, attributes=attributes, **kwargs) if attributes is None: attributes = {} for a in attributes: setattr(self, a, attributes[a]) if self.created_at is None: self.created_at = jh.now_to_timestamp() if jh.is_live() and config['env']['notifications']['events']['submitted_orders']: self.notify_submission() if jh.is_debuggable('order_submission'): logger.info( '{} order: {}, {}, {}, {}, ${}'.format( 'QUEUED' if self.is_queued else 'SUBMITTED', self.symbol, self.type, self.side, self.qty, round(self.price, 2) ) ) # handle exchange balance for ordered asset e = selectors.get_exchange(self.exchange) e.on_order_submission(self)
def _close(self, close_price: float) -> None: if self.is_open is False: raise EmptyPosition('The position is already closed.') # just to prevent confusion close_qty = abs(self.qty) estimated_profit = jh.estimate_PNL(close_qty, self.entry_price, close_price, self.type) entry = self.entry_price trade_type = self.type self.exit_price = close_price if self.exchange: self.exchange.add_realized_pnl(estimated_profit) self.exchange.temp_reduced_amount[jh.base_asset( self.symbol)] += abs(close_qty * close_price) self.qty = 0 self.entry_price = None self.closed_at = jh.now_to_timestamp() if not jh.is_unit_testing(): info_text = f'CLOSED {trade_type} position: {self.exchange_name}, {self.symbol}, {self.strategy.name}. PNL: ${round(estimated_profit, 2)}, Balance: ${jh.format_currency(round(self.exchange.wallet_balance(self.symbol), 2))}, entry: {entry}, exit: {close_price}' if jh.is_debuggable('position_closed'): logger.info(info_text) if jh.is_live( ) and config['env']['notifications']['events']['updated_position']: notifier.notify(info_text)
def execute(self, silent=False) -> None: if self.is_canceled or self.is_executed: return self.executed_at = jh.now_to_timestamp() self.status = order_statuses.EXECUTED if jh.is_live(): self.save() if not silent: txt = f'EXECUTED order: {self.symbol}, {self.type}, {self.side}, {self.qty}' if self.price: txt += f', ${round(self.price, 2)}' # log if jh.is_debuggable('order_execution'): logger.info(txt) # notify if jh.is_live(): self.broadcast() if config['env']['notifications']['events']['executed_orders']: notify(txt) p = selectors.get_position(self.exchange, self.symbol) if p: p._on_executed_order(self) # handle exchange balance for ordered asset e = selectors.get_exchange(self.exchange) e.on_order_execution(self)
def __init__(self, attributes: dict = None, **kwargs) -> None: Model.__init__(self, attributes=attributes, **kwargs) if attributes is None: attributes = {} for a, value in attributes.items(): setattr(self, a, value) if self.created_at is None: self.created_at = jh.now_to_timestamp() if jh.is_live(): from jesse.store import store self.session_id = store.app.session_id self.save(force_insert=True) if jh.is_live(): self.notify_submission() if jh.is_debuggable('order_submission'): txt = f'{"QUEUED" if self.is_queued else "SUBMITTED"} order: {self.symbol}, {self.type}, {self.side}, {self.qty}' if self.price: txt += f', ${round(self.price, 2)}' logger.info(txt) # handle exchange balance for ordered asset e = selectors.get_exchange(self.exchange) e.on_order_submission(self)
def _open(self, qty, price, change_balance=True): if self.is_open: raise OpenPositionError( 'an already open position cannot be opened') # if change_balance: # size = abs(qty) * price # if self.exchange: # self.exchange.decrease_margin_balance(size) self.entry_price = price self.exit_price = None self.qty = qty self.opened_at = jh.now() info_text = 'OPENED {} position: {}, {}, {}, ${}'.format( self.type, self.exchange_name, self.symbol, self.qty, round(self.entry_price, 2)) if jh.is_debuggable('position_opened'): logger.info(info_text) if jh.is_live( ) and config['env']['notifications']['events']['updated_position']: notifier.notify(info_text)
def _increase(self, qty, price): if not self.is_open: raise OpenPositionError( 'position must be already open in order to increase its size') qty = abs(qty) size = qty * price # if self.exchange: # self.exchange.decrease_margin_balance(size) self.entry_price = jh.estimate_average_price(qty, price, self.qty, self.entry_price) if self.type == trade_types.LONG: self.qty = sum_floats(self.qty, qty) elif self.type == trade_types.SHORT: self.qty = subtract_floats(self.qty, qty) info_text = 'INCREASED position: {}, {}, {}, {}, ${}'.format( self.exchange_name, self.symbol, self.type, self.qty, round(self.entry_price, 2)) if jh.is_debuggable('position_increased'): logger.info(info_text) if jh.is_live( ) and config['env']['notifications']['events']['updated_position']: notifier.notify(info_text)
def _reduce(self, qty, price): if self.is_open is False: raise EmptyPosition('The position is closed.') # just to prevent confusion qty = abs(qty) estimated_profit = jh.estimate_PNL(qty, self.entry_price, price, self.type) if self.exchange: # self.exchange.increase_margin_balance(qty * self.entry_price + estimated_profit) self.exchange.add_realized_pnl(estimated_profit) self.exchange.temp_reduced_amount[jh.base_asset( self.symbol)] += abs(qty * price) if self.type == trade_types.LONG: self.qty = subtract_floats(self.qty, qty) elif self.type == trade_types.SHORT: self.qty = sum_floats(self.qty, qty) info_text = 'REDUCED position: {}, {}, {}, {}, ${}'.format( self.exchange_name, self.symbol, self.type, self.qty, round(self.entry_price, 2)) if jh.is_debuggable('position_reduced'): logger.info(info_text) if jh.is_live( ) and config['env']['notifications']['events']['updated_position']: notifier.notify(info_text)
def _close(self, close_price): if self.is_open is False: raise EmptyPosition('The position is already closed.') # just to prevent confusion close_qty = abs(self.qty) estimated_profit = jh.estimate_PNL(close_qty, self.entry_price, close_price, self.type) entry = self.entry_price trade_type = self.type self.exit_price = close_price if self.exchange: self.exchange.add_realized_pnl(estimated_profit) self.exchange.temp_reduced_amount[jh.base_asset( self.symbol)] += abs(close_qty * close_price) self.qty = 0 self.entry_price = None self.closed_at = jh.now() info_text = 'CLOSED {} position: {}, {}. PNL: ${}, entry: {}, exit: {}'.format( trade_type, self.exchange_name, self.symbol, round(estimated_profit, 2), entry, close_price) if jh.is_debuggable('position_closed'): logger.info(info_text) if jh.is_live( ) and config['env']['notifications']['events']['updated_position']: notifier.notify(info_text)
def _reduce(self, qty, price): if self.is_open is False: raise EmptyPosition('The position is closed.') # just to prevent confusion qty = abs(qty) estimated_profit = jh.estimate_PNL(qty, self.entry_price, price, self.type) if self.exchange: self.exchange.increase_balance( self, qty * self.entry_price + estimated_profit) if self.type == trade_types.LONG: self.qty -= qty elif self.type == trade_types.SHORT: self.qty += qty info_text = 'REDUCED position: {}, {}, {}, {}, ${}'.format( self.exchange_name, self.symbol, self.type, self.qty, round(self.entry_price, 2) ) if jh.is_debuggable('position_reduced'): logger.info(info_text) if jh.is_live() and config['env']['notifications']['events']['updated_position']: notifier.notify(info_text)
def on_order_submission(self, order: Order, skip_market_order=True): base_asset = jh.base_asset(order.symbol) quote_asset = jh.quote_asset(order.symbol) # skip market order at the time of submission because we don't have # the exact order.price. Instead, we call on_order_submission() one # more time at time of execution without "skip_market_order=False". if order.type == order_types.MARKET and skip_market_order: return # used for logging balance change temp_old_quote_available_asset = self.available_assets[quote_asset] temp_old_base_available_asset = self.available_assets[base_asset] if order.side == sides.BUY: quote_balance = self.available_assets[quote_asset] self.available_assets[quote_asset] -= ( abs(order.qty) * order.price) * (1 + self.fee_rate) if self.available_assets[quote_asset] < 0: raise NegativeBalance( f"Balance cannot go below zero in spot market. Available capital at {self.name} for {quote_asset} is {quote_balance} but you're trying to sell {abs(order.qty * order.price)}" ) # sell order else: base_balance = self.available_assets[base_asset] new_base_balance = base_balance + order.qty if new_base_balance < 0: raise NegativeBalance( f"Balance cannot go below zero in spot market. Available capital at {self.name} for {base_asset} is {base_balance} but you're trying to sell {abs(order.qty)}" ) self.available_assets[base_asset] -= abs(order.qty) temp_new_quote_available_asset = self.available_assets[quote_asset] if jh.is_debuggable( 'balance_update' ) and temp_old_quote_available_asset != temp_new_quote_available_asset: logger.info( f'Available balance for {quote_asset} on {self.name} changed from {round(temp_old_quote_available_asset, 2)} to {round(temp_new_quote_available_asset, 2)}' ) temp_new_base_available_asset = self.available_assets[base_asset] if jh.is_debuggable( 'balance_update' ) and temp_old_base_available_asset != temp_new_base_available_asset: logger.info( f'Available balance for {base_asset} on {self.name} changed from {round(temp_old_base_available_asset, 2)} to {round(temp_new_base_available_asset, 2)}' )
def decrease_balance(self, position, balance): old_balance = self.balance self.balance -= abs(balance) * (1 + self.fee) new_balance = self.balance if jh.is_debuggable('balance_update'): logger.info('balance changed from {} to {}'.format( old_balance, new_balance))
def increase_balance(self, position, balance, is_refund=False): old_balance = self.balance if is_refund: self.balance += abs(balance) * (1 + self.fee) else: self.balance += abs(balance) * (1 - self.fee) new_balance = self.balance if jh.is_debuggable('balance_update'): logger.info('balance changed from {} to {}'.format( old_balance, new_balance))
def __init__(self, attributes=None): # id generated by Jesse for database usage self.id = '' # id generated by market, used in live-trade mode self.exchange_id = '' # some exchanges might require even further info self.vars = {} self.symbol = '' self.exchange = '' self.side = '' self.type = '' self.flag = '' self.qty = 0 self.price = 0 self.status = order_statuses.ACTIVE self.created_at = None self.executed_at = None self.canceled_at = None self.role = None if attributes is None: attributes = {} for a in attributes: setattr(self, a, attributes[a]) if self.created_at is None: self.created_at = jh.now_to_timestamp() p = selectors.get_position(self.exchange, self.symbol) if p: if jh.is_live() and config['env']['notifications']['events']['submitted_orders']: self.notify_submission() if jh.is_debuggable('order_submission'): logger.info( '{} order: {}, {}, {}, {}, ${}'.format( 'QUEUED' if self.is_queued else 'SUBMITTED', self.symbol, self.type, self.side, self.qty, round(self.price, 2) ) ) p._on_opened_order(self) # handle exchange balance for ordered asset e = selectors.get_exchange(self.exchange) e.on_order_submission(self)
def _open(self, qty: float, price: float, change_balance: bool = True) -> None: if self.is_open: raise OpenPositionError('an already open position cannot be opened') self.entry_price = price self.exit_price = None self.qty = qty self.opened_at = jh.now_to_timestamp() info_text = f'OPENED {self.type} position: {self.exchange_name}, {self.symbol}, {self.qty}, ${round(self.entry_price, 2)}' if jh.is_debuggable('position_opened'): logger.info(info_text) if jh.is_live() and config['env']['notifications']['events']['updated_position']: notifier.notify(info_text)
def decrease_balance(self, position, delta_balance): old_balance = self.balance to_spend = abs(delta_balance) * (1 + self.fee_rate) self.balance -= to_spend new_balance = self.balance if new_balance < 0: raise NegativeBalance( "Balance cannot go below zero. Available capital at {} is {} but you're trying to spend {}" .format(self.name, old_balance, to_spend)) if jh.is_debuggable('balance_update'): logger.info('balance changed from {} to {}'.format( old_balance, new_balance))
def increase_balance(self, position, delta_balance, is_refund=False): """ :param position: :param delta_balance: :param is_refund: """ old_balance = self.balance if is_refund: self.balance += abs(delta_balance) * (1 + self.fee_rate) else: self.balance += abs(delta_balance) * (1 - self.fee_rate) new_balance = self.balance if jh.is_debuggable('balance_update'): logger.info('balance changed from {} to {}'.format(old_balance, new_balance))
def __init__(self, attributes=None): # id generated by Jesse for database usage self.id = '' # id generated by market self.exchange_id = '' self.symbol = '' self.exchange = '' self.side = '' self.type = '' self.flag = '' self.qty = 0 self.price = 0 self.status = order_statuses.ACTIVE self.created_at = None self.executed_at = None self.canceled_at = None self.role = None if attributes is None: attributes = {} for a in attributes: setattr(self, a, attributes[a]) if self.created_at is None: self.created_at = jh.now() p = selectors.get_position(self.exchange, self.symbol) if p: p._on_opened_order(self) if jh.is_live() and config['env']['notifications']['events']['submitted_orders']: self.notify_submission() if jh.is_debuggable('order_submission'): logger.info( '{} order: {}, {}, {}, {}, ${}'.format( 'QUEUED' if self.is_queued else 'SUBMITTED', self.symbol, self.type, self.side, self.qty, round(self.price, 2) ) )
def cancel(self): if self.is_canceled or self.is_executed: return self.canceled_at = jh.now_to_timestamp() self.status = order_statuses.CANCELED if jh.is_debuggable('order_cancellation'): logger.info('CANCELED order: {}, {}, {}, {}, ${}'.format( self.symbol, self.type, self.side, self.qty, round(self.price, 2))) if jh.is_live( ) and config['env']['notifications']['events']['cancelled_orders']: notify('CANCELED order: {}, {}, {}, {}, {}'.format( self.symbol, self.type, self.side, self.qty, round(self.price, 2))) # handle exchange balance e = selectors.get_exchange(self.exchange) e.on_order_cancellation(self)
def __init__(self, attributes: dict = None, **kwargs) -> None: Model.__init__(self, attributes=attributes, **kwargs) if attributes is None: attributes = {} for a, value in attributes.items(): setattr(self, a, value) if self.created_at is None: self.created_at = jh.now_to_timestamp() if jh.is_live( ) and config['env']['notifications']['events']['submitted_orders']: self.notify_submission() if jh.is_debuggable('order_submission'): logger.info( f'{"QUEUED" if self.is_queued else "SUBMITTED"} order: {self.symbol}, {self.type}, {self.side}, {self.qty}, ${round(self.price, 2)}' ) # handle exchange balance for ordered asset e = selectors.get_exchange(self.exchange) e.on_order_submission(self)
def run(start_date: str, finish_date: str, candles: Dict[str, Dict[str, Union[str, np.ndarray]]] = None, chart: bool = False, tradingview: bool = False, full_reports: bool = False, csv: bool = False, json: bool = False) -> None: # clear the screen if not jh.should_execute_silently(): click.clear() # validate routes validate_routes(router) # initiate candle store store.candles.init_storage(5000) # load historical candles if candles is None: print('loading candles...') candles = load_candles(start_date, finish_date) click.clear() if not jh.should_execute_silently(): # print candles table key = '{}-{}'.format(config['app']['considering_candles'][0][0], config['app']['considering_candles'][0][1]) table.key_value(stats.candles(candles[key]['candles']), 'candles', alignments=('left', 'right')) print('\n') # print routes table table.multi_value(stats.routes(router.routes)) print('\n') # print guidance for debugging candles if jh.is_debuggable('trading_candles') or jh.is_debuggable( 'shorter_period_candles'): print( ' Symbol | timestamp | open | close | high | low | volume' ) # run backtest simulation simulator(candles) if not jh.should_execute_silently(): # print trades metrics if store.completed_trades.count > 0: change = [] # calcualte market change for e in router.routes: if e.strategy is None: return first = Candle.select(Candle.close).where( Candle.timestamp == jh.date_to_timestamp(start_date), Candle.exchange == e.exchange, Candle.symbol == e.symbol).first() last = Candle.select(Candle.close).where( Candle.timestamp == jh.date_to_timestamp(finish_date) - 60000, Candle.exchange == e.exchange, Candle.symbol == e.symbol).first() change.append( ((last.close - first.close) / first.close) * 100.0) data = report.portfolio_metrics() data.append( ['Market Change', str(round(np.average(change), 2)) + "%"]) print('\n') table.key_value(data, 'Metrics', alignments=('left', 'right')) print('\n') # save logs store_logs(json, tradingview, csv) if chart: charts.portfolio_vs_asset_returns() # QuantStats' report if full_reports: quantstats.quantstats_tearsheet() else: print(jh.color('No trades were made.', 'yellow'))
def simulator(candles: Dict[str, Dict[str, Union[str, np.ndarray]]], hyperparameters=None) -> None: begin_time_track = time.time() key = '{}-{}'.format(config['app']['considering_candles'][0][0], config['app']['considering_candles'][0][1]) first_candles_set = candles[key]['candles'] length = len(first_candles_set) # to preset the array size for performance store.app.starting_time = first_candles_set[0][0] store.app.time = first_candles_set[0][0] # initiate strategies for r in router.routes: StrategyClass = jh.get_strategy_class(r.strategy_name) try: r.strategy = StrategyClass() except TypeError: raise exceptions.InvalidStrategy( "Looks like the structure of your strategy directory is incorrect. Make sure to include the strategy INSIDE the __init__.py file." "\nIf you need working examples, check out: https://github.com/jesse-ai/example-strategies" ) except: raise r.strategy.name = r.strategy_name r.strategy.exchange = r.exchange r.strategy.symbol = r.symbol r.strategy.timeframe = r.timeframe # inject hyper parameters (used for optimize_mode) # convert DNS string into hyperparameters if r.dna and hyperparameters is None: hyperparameters = jh.dna_to_hp(r.strategy.hyperparameters(), r.dna) # inject hyperparameters sent within the optimize mode if hyperparameters is not None: r.strategy.hp = hyperparameters # init few objects that couldn't be initiated in Strategy __init__ # it also injects hyperparameters into self.hp in case the route does not uses any DNAs r.strategy._init_objects() selectors.get_position(r.exchange, r.symbol).strategy = r.strategy # add initial balance save_daily_portfolio_balance() with click.progressbar(length=length, label='Executing simulation...') as progressbar: for i in range(length): # update time store.app.time = first_candles_set[i][0] + 60_000 # add candles for j in candles: short_candle = candles[j]['candles'][i] if i != 0: previous_short_candle = candles[j]['candles'][i - 1] short_candle = _get_fixed_jumped_candle( previous_short_candle, short_candle) exchange = candles[j]['exchange'] symbol = candles[j]['symbol'] store.candles.add_candle(short_candle, exchange, symbol, '1m', with_execution=False, with_generation=False) # print short candle if jh.is_debuggable('shorter_period_candles'): print_candle(short_candle, True, symbol) _simulate_price_change_effect(short_candle, exchange, symbol) # generate and add candles for bigger timeframes for timeframe in config['app']['considering_timeframes']: # for 1m, no work is needed if timeframe == '1m': continue count = jh.timeframe_to_one_minutes(timeframe) until = count - ((i + 1) % count) if (i + 1) % count == 0: generated_candle = generate_candle_from_one_minutes( timeframe, candles[j]['candles'][(i - (count - 1)):(i + 1)]) store.candles.add_candle(generated_candle, exchange, symbol, timeframe, with_execution=False, with_generation=False) # update progressbar if not jh.is_debugging() and not jh.should_execute_silently( ) and i % 60 == 0: progressbar.update(60) # now that all new generated candles are ready, execute for r in router.routes: count = jh.timeframe_to_one_minutes(r.timeframe) # 1m timeframe if r.timeframe == timeframes.MINUTE_1: r.strategy._execute() elif (i + 1) % count == 0: # print candle if jh.is_debuggable('trading_candles'): print_candle( store.candles.get_current_candle( r.exchange, r.symbol, r.timeframe), False, r.symbol) r.strategy._execute() # now check to see if there's any MARKET orders waiting to be executed store.orders.execute_pending_market_orders() if i != 0 and i % 1440 == 0: save_daily_portfolio_balance() if not jh.should_execute_silently(): if jh.is_debuggable('trading_candles') or jh.is_debuggable( 'shorter_period_candles'): print('\n') # print executed time for the backtest session finish_time_track = time.time() print( 'Executed backtest simulation in: ', '{} seconds'.format(round(finish_time_track - begin_time_track, 2))) for r in router.routes: r.strategy._terminate() store.orders.execute_pending_market_orders() # now that backtest is finished, add finishing balance save_daily_portfolio_balance()
def simulator( candles: dict, run_silently: bool, hyperparameters: dict = None ) -> None: begin_time_track = time.time() key = f"{config['app']['considering_candles'][0][0]}-{config['app']['considering_candles'][0][1]}" first_candles_set = candles[key]['candles'] length = len(first_candles_set) # to preset the array size for performance try: store.app.starting_time = first_candles_set[0][0] except IndexError: raise IndexError('Check your "warm_up_candles" config value') store.app.time = first_candles_set[0][0] # initiate strategies for r in router.routes: # if the r.strategy is str read it from file if isinstance(r.strategy_name, str): StrategyClass = jh.get_strategy_class(r.strategy_name) # else it is a class object so just use it else: StrategyClass = r.strategy_name try: r.strategy = StrategyClass() except TypeError: raise exceptions.InvalidStrategy( "Looks like the structure of your strategy directory is incorrect. Make sure to include the strategy INSIDE the __init__.py file." "\nIf you need working examples, check out: https://github.com/jesse-ai/example-strategies" ) except: raise r.strategy.name = r.strategy_name r.strategy.exchange = r.exchange r.strategy.symbol = r.symbol r.strategy.timeframe = r.timeframe # read the dna from strategy's dna() and use it for injecting inject hyperparameters # first convert DNS string into hyperparameters if len(r.strategy.dna()) > 0 and hyperparameters is None: hyperparameters = jh.dna_to_hp(r.strategy.hyperparameters(), r.strategy.dna()) # inject hyperparameters sent within the optimize mode if hyperparameters is not None: r.strategy.hp = hyperparameters # init few objects that couldn't be initiated in Strategy __init__ # it also injects hyperparameters into self.hp in case the route does not uses any DNAs r.strategy._init_objects() selectors.get_position(r.exchange, r.symbol).strategy = r.strategy # add initial balance save_daily_portfolio_balance() progressbar = Progressbar(length, step=60) for i in range(length): # update time store.app.time = first_candles_set[i][0] + 60_000 # add candles for j in candles: short_candle = candles[j]['candles'][i] if i != 0: previous_short_candle = candles[j]['candles'][i - 1] short_candle = _get_fixed_jumped_candle(previous_short_candle, short_candle) exchange = candles[j]['exchange'] symbol = candles[j]['symbol'] store.candles.add_candle(short_candle, exchange, symbol, '1m', with_execution=False, with_generation=False) # print short candle if jh.is_debuggable('shorter_period_candles'): print_candle(short_candle, True, symbol) _simulate_price_change_effect(short_candle, exchange, symbol) # generate and add candles for bigger timeframes for timeframe in config['app']['considering_timeframes']: # for 1m, no work is needed if timeframe == '1m': continue count = jh.timeframe_to_one_minutes(timeframe) # until = count - ((i + 1) % count) if (i + 1) % count == 0: generated_candle = generate_candle_from_one_minutes( timeframe, candles[j]['candles'][(i - (count - 1)):(i + 1)]) store.candles.add_candle(generated_candle, exchange, symbol, timeframe, with_execution=False, with_generation=False) # update progressbar if not run_silently and i % 60 == 0: progressbar.update() sync_publish('progressbar', { 'current': progressbar.current, 'estimated_remaining_seconds': progressbar.estimated_remaining_seconds }) # now that all new generated candles are ready, execute for r in router.routes: count = jh.timeframe_to_one_minutes(r.timeframe) # 1m timeframe if r.timeframe == timeframes.MINUTE_1: r.strategy._execute() elif (i + 1) % count == 0: # print candle if jh.is_debuggable('trading_candles'): print_candle(store.candles.get_current_candle(r.exchange, r.symbol, r.timeframe), False, r.symbol) r.strategy._execute() # now check to see if there's any MARKET orders waiting to be executed store.orders.execute_pending_market_orders() if i != 0 and i % 1440 == 0: save_daily_portfolio_balance() if not run_silently: # print executed time for the backtest session finish_time_track = time.time() sync_publish('alert', { 'message': f'Successfully executed backtest simulation in: {round(finish_time_track - begin_time_track, 2)} seconds', 'type': 'success' }) for r in router.routes: r.strategy._terminate() store.orders.execute_pending_market_orders() # now that backtest is finished, add finishing balance save_daily_portfolio_balance()
def test_is_debuggable(): debug_item = 'order_submission' assert jh.is_debuggable(debug_item) is False
def generate_signal(start_date: str, finish_date: str, candles=None, chart=False, tradingview=False, csv=False, json=False): # clear the screen if not jh.should_execute_silently(): click.clear() # validate routes validate_routes(router) # initiate candle store store.candles.init_storage(5000) # load historical candles if candles is None: print('loading candles...') candles = load_candles(start_date, finish_date) click.clear() if not jh.should_execute_silently(): # print candles table key = '{}-{}'.format(config['app']['considering_candles'][0][0], config['app']['considering_candles'][0][1]) table.key_value(stats.candles(candles[key]['candles']), 'candles', alignments=('left', 'right')) print('\n') # print routes table table.multi_value(stats.routes(router.routes)) print('\n') # print guidance for debugging candles if jh.is_debuggable('trading_candles') or jh.is_debuggable( 'shorter_period_candles'): print( ' Symbol | timestamp | open | close | high | low | volume' ) # run backtest simulation signal_simulator(candles) if not jh.should_execute_silently(): # print trades statistics if store.completed_trades.count > 0: print('\n') table.key_value(report.portfolio_metrics(), 'Metrics', alignments=('left', 'right')) print('\n') # save logs store_logs(json, tradingview, csv) if chart: charts.portfolio_vs_asset_returns() # # clone the trades so that the original is not mutated # completed_trades = copy.deepcopy(store.completed_trades.trades) # # for trade in completed_trades: # print(trade.to_dict()) # # # filter trades which were generated for current day # completed_trades = filter(lambda x: x.opened_at == jh.get_current_time_in_epoch(), completed_trades) # # # TODO: add a slack notification here instead of print to notify the user # print("Trades to execute today: ") # for trade in completed_trades: # print(trade.to_dict()) else: print(jh.color('No trades were completed/closed.', 'yellow')) open_positions = store.positions.get_open_positions() open_orders = store.orders.get_all_active_orders() current_report = jh.generate_signals_report(open_positions, open_orders) return current_report
def run(start_date: str, finish_date: str, candles=None, chart=False, tradingview=False, csv=False, json=False): # clear the screen if not jh.should_execute_silently(): click.clear() # validate routes validate_routes(router) # initiate candle store store.candles.init_storage(5000) # load historical candles if candles is None: print('loading candles...') candles = load_candles(start_date, finish_date) click.clear() if not jh.should_execute_silently(): # print candles table key = '{}-{}'.format(config['app']['trading_exchanges'][0], config['app']['trading_symbols'][0]) table.key_value(stats.candles(candles[key]['candles']), 'candles', alignments=('left', 'right')) print('\n') # print routes table table.multi_value(stats.routes(router.routes)) print('\n') # print guidance for debugging candles if jh.is_debuggable('trading_candles') or jh.is_debuggable( 'shorter_period_candles'): print( ' Symbol | timestamp | open | close | high | low | volume' ) # run backtest simulation simulator(candles) if not jh.should_execute_silently(): # print trades statistics if store.completed_trades.count > 0: print('\n') table.key_value(report.portfolio_metrics(), 'Metrics', alignments=('left', 'right')) print('\n') # save logs store_logs(json, tradingview, csv) if chart: charts.portfolio_vs_asset_returns() else: print(jh.color('No trades were made.', 'yellow'))
def run(start_date: str, finish_date: str, candles: Dict[str, Dict[str, Union[str, np.ndarray]]] = None, chart: bool = False, tradingview: bool = False, full_reports: bool = False, csv: bool = False, json: bool = False) -> None: # clear the screen if not jh.should_execute_silently(): click.clear() # validate routes validate_routes(router) # initiate candle store store.candles.init_storage(5000) # load historical candles if candles is None: print('loading candles...') candles = load_candles(start_date, finish_date) click.clear() if not jh.should_execute_silently(): # print candles table key = f"{config['app']['considering_candles'][0][0]}-{config['app']['considering_candles'][0][1]}" table.key_value(stats.candles(candles[key]['candles']), 'candles', alignments=('left', 'right')) print('\n') # print routes table table.multi_value(stats.routes(router.routes)) print('\n') # print guidance for debugging candles if jh.is_debuggable('trading_candles') or jh.is_debuggable( 'shorter_period_candles'): print( ' Symbol | timestamp | open | close | high | low | volume' ) # run backtest simulation simulator(candles) if not jh.should_execute_silently(): # print trades metrics if store.completed_trades.count > 0: change = [] # calcualte market change for e in router.routes: if e.strategy is None: return first = Candle.select(Candle.close).where( Candle.timestamp == jh.date_to_timestamp(start_date), Candle.exchange == e.exchange, Candle.symbol == e.symbol).first() last = Candle.select(Candle.close).where( Candle.timestamp == jh.date_to_timestamp(finish_date) - 60000, Candle.exchange == e.exchange, Candle.symbol == e.symbol).first() change.append( ((last.close - first.close) / first.close) * 100.0) data = report.portfolio_metrics() data.append( ['Market Change', f"{str(round(np.average(change), 2))}%"]) print('\n') table.key_value(data, 'Metrics', alignments=('left', 'right')) print('\n') # save logs more = "" routes_count = len(router.routes) if routes_count > 1: more = f"-and-{routes_count-1}-more" study_name = f"{router.routes[0].strategy_name}-{router.routes[0].exchange}-{router.routes[0].symbol}-{router.routes[0].timeframe}{more}-{start_date}-{finish_date}" store_logs(study_name, json, tradingview, csv) if chart: charts.portfolio_vs_asset_returns(study_name) # QuantStats' report if full_reports: price_data = [] # load close candles for Buy and hold and calculate pct_change for index, c in enumerate( config['app']['considering_candles']): exchange, symbol = c[0], c[1] if exchange in config['app'][ 'trading_exchanges'] and symbol in config['app'][ 'trading_symbols']: # fetch from database candles_tuple = Candle.select( Candle.timestamp, Candle.close).where( Candle.timestamp.between( jh.date_to_timestamp(start_date), jh.date_to_timestamp(finish_date) - 60000), Candle.exchange == exchange, Candle.symbol == symbol).order_by( Candle.timestamp.asc()).tuples() candles = np.array(candles_tuple) timestamps = candles[:, 0] price_data.append(candles[:, 1]) price_data = np.transpose(price_data) price_df = pd.DataFrame(price_data, index=pd.to_datetime(timestamps, unit="ms"), dtype=float).resample('D').mean() price_pct_change = price_df.pct_change(1).fillna(0) bh_daily_returns_all_routes = price_pct_change.mean(1) quantstats.quantstats_tearsheet(bh_daily_returns_all_routes, study_name) else: print(jh.color('No trades were made.', 'yellow'))
def on_order_execution(self, order: Order): base_asset = jh.base_asset(order.symbol) quote_asset = jh.quote_asset(order.symbol) if order.type == order_types.MARKET: self.on_order_submission(order, skip_market_order=False) if self.type == 'margin': if order.side == sides.BUY: # find and set order to [0, 0] (same as removing it) for index, item in enumerate(self.buy_orders[base_asset]): if item[0] == order.qty and item[1] == order.price: self.buy_orders[base_asset][index] = np.array([0, 0]) break else: # find and set order to [0, 0] (same as removing it) for index, item in enumerate(self.sell_orders[base_asset]): if item[0] == order.qty and item[1] == order.price: self.sell_orders[base_asset][index] = np.array([0, 0]) break return # used for logging balance change temp_old_quote_asset = self.assets[quote_asset] temp_old_quote_available_asset = self.available_assets[quote_asset] temp_old_base_asset = self.assets[base_asset] temp_old_base_available_asset = self.available_assets[base_asset] # works for both buy and sell orders (sell order's qty < 0) self.assets[base_asset] += order.qty if order.side == sides.BUY: self.available_assets[base_asset] += order.qty self.assets[quote_asset] -= (abs(order.qty) * order.price) * (1 + self.fee_rate) # sell order else: self.available_assets[quote_asset] += abs( order.qty) * order.price * (1 - self.fee_rate) self.assets[quote_asset] += abs( order.qty) * order.price * (1 - self.fee_rate) temp_new_quote_asset = self.assets[quote_asset] if jh.is_debuggable('balance_update' ) and temp_old_quote_asset != temp_new_quote_asset: logger.info('Balance for {} on {} changed from {} to {}'.format( quote_asset, self.name, round(temp_old_quote_asset, 2), round(temp_new_quote_asset, 2))) temp_new_quote_available_asset = self.available_assets[quote_asset] if jh.is_debuggable( 'balance_update' ) and temp_old_quote_available_asset != temp_new_quote_available_asset: logger.info('Balance for {} on {} changed from {} to {}'.format( quote_asset, self.name, round(temp_old_quote_available_asset, 2), round(temp_new_quote_available_asset, 2))) temp_new_base_asset = self.assets[base_asset] if jh.is_debuggable('balance_update' ) and temp_old_base_asset != temp_new_base_asset: logger.info('Balance for {} on {} changed from {} to {}'.format( base_asset, self.name, round(temp_old_base_asset, 2), round(temp_new_base_asset, 2))) temp_new_base_available_asset = self.available_assets[base_asset] if jh.is_debuggable( 'balance_update' ) and temp_old_base_available_asset != temp_new_base_available_asset: logger.info('Balance for {} on {} changed from {} to {}'.format( base_asset, self.name, round(temp_old_base_available_asset, 2), round(temp_new_base_available_asset, 2)))
def on_order_submission(self, order: Order, skip_market_order=True): base_asset = jh.base_asset(order.symbol) quote_asset = jh.quote_asset(order.symbol) # skip market order at the time of submission because we don't have # the exact order.price. Instead, we call on_order_submission() one # more time at time of execution without "skip_market_order=False". if order.type == order_types.MARKET and skip_market_order: return # in margin, we only update available_asset's value which is used for detecting reduce_only orders if self.type == 'margin': self.available_assets[base_asset] += order.qty if order.side == sides.BUY: self.buy_orders[base_asset].append( np.array([order.qty, order.price])) else: self.sell_orders[base_asset].append( np.array([order.qty, order.price])) # self.available_assets[quote_asset] -= order.qty * order.price return # used for logging balance change temp_old_quote_available_asset = self.available_assets[quote_asset] temp_old_base_available_asset = self.available_assets[base_asset] if order.side == sides.BUY: quote_balance = self.available_assets[quote_asset] self.available_assets[quote_asset] -= ( abs(order.qty) * order.price) * (1 + self.fee_rate) if self.available_assets[quote_asset] < 0: raise NegativeBalance( "Balance cannot go below zero in spot market. Available capital at {} for {} is {} but you're trying to sell {}" .format(self.name, quote_asset, quote_balance, abs(order.qty * order.price))) # sell order else: base_balance = self.available_assets[base_asset] new_base_balance = base_balance + order.qty if new_base_balance < 0: raise NegativeBalance( "Balance cannot go below zero in spot market. Available capital at {} for {} is {} but you're trying to sell {}" .format(self.name, base_asset, base_balance, abs(order.qty))) self.available_assets[base_asset] -= abs(order.qty) temp_new_quote_available_asset = self.available_assets[quote_asset] if jh.is_debuggable( 'balance_update' ) and temp_old_quote_available_asset != temp_new_quote_available_asset: logger.info( 'Available balance for {} on {} changed from {} to {}'.format( quote_asset, self.name, round(temp_old_quote_available_asset, 2), round(temp_new_quote_available_asset, 2))) temp_new_base_available_asset = self.available_assets[base_asset] if jh.is_debuggable( 'balance_update' ) and temp_old_base_available_asset != temp_new_base_available_asset: logger.info( 'Available balance for {} on {} changed from {} to {}'.format( base_asset, self.name, round(temp_old_base_available_asset, 2), round(temp_new_base_available_asset, 2)))