def test_update_ticker() -> None: pairs = ['GBPUSD', 'USDJPY'] time = pd.Timestamp('2020-07-09 12:23:10') ticker = Ticker(pairs) # pair, ask, bid, GBPUSD(ask, bid), USDGBP(ask, bid), # USDJPY(ask, bid), JPYUSD(ask, bid) tick1 = [ 'GBPUSD', '1.50349', '1.30328', '1.50349', '1.30328', '0.76729483', '0.66511916', '0', '0', '0', '0' ] tick2 = [ 'USDJPY', '110.863', '105.774', '1.50349', '1.30328', '0.76729483', '0.66511916', '110.863', '105.774', '0.00945412', '0.00902014' ] tick3 = [ 'GBPUSD', '1.2543', '1.2541', '1.2543', '1.2541', '0.79738458', '0.79725743', '110.863', '105.774', '0.00945412', '0.00902014' ] tick4 = [ 'USDJPY', '107.8', '107.25', '1.2543', '1.2541', '0.79738458', '0.79725743', '107.8', '107.25', '0.00932401', '0.00927644' ] tick = [tick1, tick2, tick3, tick4] for t in tick: event = TickEvent(t[0], time, Decimal(t[2]), Decimal(t[1])) ticker.update_ticker(event) result = [] for p in ['GBPUSD', 'USDGBP', 'USDJPY', 'JPYUSD']: for q in ['ask', 'bid']: result.append(ticker.prices[p][q]) assert result == list(map(Decimal, t[3:]))
class Engine(object): """ Enscapsulates the settings and components for carrying out an event-driven trading/backtest on the foreign exchange markets. """ logger: Logger pairs: List[Pair] datafeed: DataFeeder execution: ExecutionHandler strategy: Strategy result: ResultHandler portfolio: Portfolio ticker: Ticker equity: Decimal home_currency: str heartbeat: float max_iters: int iters: int event_q: 'Queue[Event]' feed_q: 'Queue[Event]' exec_q: 'Queue[Event]' result_q: 'Queue[Result]' def __init__(self, engine: engine_params, datafeed: datafeed_params, execution: execution_params, strategy: strategy_params, result: result_params): """ Initializes the backtest. """ self.logger = getLogger(__name__) self.pairs = engine['pairs'] self.home_currency = engine['home_currency'] self.equity = engine['equity'] self.isBacktest = engine['isBacktest'] self.max_iters = engine['max_iters'] self.heartbeat = engine['heart_beat'] self.event_q = Queue() self.feed_q = Queue() self.exec_q = Queue() self.result_q = Queue() self.iters = 0 self.datafeed = self._setup_datafeed(datafeed) self.execution = self._setup_execution(execution) self.strategy = self._setup_strategy(strategy) self.result = self._setup_result(result) self.ticker = Ticker(self.pairs) self.portfolio = Portfolio(ticker=self.ticker, event_q=self.event_q, result_q=self.result_q, home_currency=self.home_currency, pairs=self.pairs, equity=self.equity) self.toContinue = True initializeDecimalContext() def _setup_datafeed(self, datafeed: datafeed_params) -> DataFeeder: _module = import_module('savoia.datafeed.datafeed') _params = datafeed['params'] _params['pairs'] = self.pairs _params['feed_q'] = self.feed_q df = getattr(_module, datafeed['module_name']) return df(**_params) def _setup_execution(self, execution: execution_params) \ -> ExecutionHandler: _module = import_module('savoia.execution.execution') _params = execution['params'] _params['event_q'] = self.event_q _params['exec_q'] = self.exec_q exe = getattr(_module, execution['module_name']) return exe(**_params) def _setup_strategy(self, strategy: strategy_params) \ -> ExecutionHandler: _module = import_module('savoia.strategy.strategy') _params = strategy['params'] _params['pairs'] = self.pairs _params['event_q'] = self.event_q exe = getattr(_module, strategy['module_name']) return exe(**_params) def _setup_result(self, result: result_params) -> ResultHandler: _module = import_module('savoia.result.result') _params = result['params'] _params['pairs'] = self.pairs _params['result_q'] = self.result_q exe = getattr(_module, result['module_name']) return exe(**_params) def _run_engine(self) -> None: """ Carries out an infinite while loop that polls the event_q queue and directs each event to either the strategy component of the execution handler. The loop will then pause for "heartbeat" seconds and continue until the maximum number of iterations is exceeded. """ _wait: bool = False self.logger.info("Running engine...") while self.iters < self.max_iters and self.toContinue: try: event = self.event_q.get(_wait) except Empty: try: tick_event = self.feed_q.get(False) except Empty: pass else: if tick_event is None: self.logger.info('Acknowledged the end of datafeed.') self.toContinue = False elif tick_event.type != 'TICK': raise Exception else: self.logger.debug('Process TICK -%s' % tick_event) self.ticker.update_ticker(tick_event) self.portfolio.update_portfolio(tick_event) self.strategy.calculate_signals(tick_event) else: _wait = False if event is not None: if event.type == 'SIGNAL': self.logger.info("Process SIGNAL -%s" % event) self.portfolio.execute_signal(event) elif event.type == 'ORDER': self.logger.info("Process ORDER -%s" % event) self.exec_q.put(event) _wait = self.isBacktest elif event.type == 'FILL': self.logger.info("Process FILL -%s" % event) self.portfolio.execute_fill(event) else: raise Exception time.sleep(self.heartbeat) self.iters += 1 self.exec_q.put(None) return def _output_performance(self) -> None: """ Outputs the strategy performance from the backtest. """ self.logger.info("Calculating Performance Metrics...") self.portfolio.output_results() def _run(self) -> None: _result = threading.Thread(target=self.result.run) _datafeed = threading.Thread(target=self.datafeed.run) _execution = threading.Thread(target=self.execution.run) _engine = threading.Thread(target=self._run_engine) _result.start() _execution.start() _engine.start() _datafeed.start() _datafeed.join() _engine.join() _execution.join() self.result_q.put(None) _result.join() def run(self) -> None: """ Runs the engine. """ if self.isBacktest: _start = time.time() self.logger.info('Start Backtesting.') self._run() # self._output_performance() self.logger.info("Backtest complete. Elapsed Time[Sec]: " + f'{time.time() - _start}') else: self.logger.info('Start Live trading.') self._run() self.logger.info("Trading complete.")