async def tick(self): """helper method to ensure periodic methods execute periodically in absence of market data""" if self._offline(): # periodics injected manually, see main event loop above return while True: yield Event(type=EventType.HEARTBEAT, target=None) await asyncio.sleep(1)
def start(self): try: # if self.event_loop.is_running(): # # return future # return asyncio.create_task(self.run()) # block until done self.event_loop.run_until_complete(self.run()) except KeyboardInterrupt: pass # send exit event to all callbacks asyncio.ensure_future( self.processEvent(Event(type=EventType.EXIT, target=None)))
async def processEvent(self, event: Event, strategy: Strategy = None) -> None: """send an event to all registered event handlers Arguments: event (Event): event to send """ if event.type == EventType.HEARTBEAT: # ignore heartbeat return for callback, handler in self._handler_subscriptions[event.type]: # TODO make cleaner? move to somewhere not in critical path? if strategy is not None and (handler not in (strategy, self.manager)): continue # TODO make cleaner? move to somewhere not in critical path? if (event.type in ( EventType.TRADE, EventType.OPEN, EventType.CHANGE, EventType.CANCEL, EventType.DATA, ) and not self.manager.dataSubscriptions(handler, event)): continue try: await callback(event) except KeyboardInterrupt: raise except SystemExit: raise except BaseException as e: if event.type == EventType.ERROR: # don't infinite error raise await self.processEvent( Event( type=EventType.ERROR, target=Error( target=event, handler=handler, callback=callback, exception=e, ), )) await asyncio.sleep(1)
def pushTrade(self, taker_order, filled_in_txn): """push taker order trade""" if not self.orders: raise Exception("No maker orders provided") if taker_order.filled <= 0: raise Exception("No trade occurred") if filled_in_txn != self.volume: raise Exception("Accumulation error occurred") self.push( Event( type=EventType.TRADE, target=Trade( volume=self.volume, price=self.price, maker_orders=self.orders.copy(), taker_order=taker_order, ), )) self._taker_order = taker_order
async def run(self): """run the engine""" # setup future queue self._queued_events = deque() self._queued_targeted_events = deque() # await all connections await asyncio.gather(*(asyncio.create_task(exch.connect()) for exch in self.exchanges)) await asyncio.gather(*(asyncio.create_task(exch.instruments()) for exch in self.exchanges)) # send start event to all callbacks await self.processEvent(Event(type=EventType.START, target=None)) # **************** # # Main event loop # **************** # async with merge( *(exch.tick() for exch in self.exchanges + [self] if inspect.isasyncgenfunction(exch.tick))).stream() as stream: # stream through all events async for event in stream: # TODO move out of critical path if self._offline(): # inject periodics # TODO optimize # Manager should keep track of the intervals for its periodics, # then we don't need to go through seconds (which is what the # live engine's `tick` method does below). Instead we can just # calculate exactly the intervals if (self._latest != datetime.fromtimestamp(0) and hasattr(event, "target") and hasattr(event.target, "timestamp")): # TODO in progress optimization intervals = self.manager.periodicIntervals() # not the first tick for _ in range( int((event.target.timestamp - self._latest).total_seconds() / intervals)): self._latest = self._latest + timedelta(seconds=1 * intervals) if any( p.expires(self._latest) for p in self.manager.periodics()): await asyncio.gather( *(asyncio.create_task( p.execute(self._latest)) for p in self.manager.periodics() if p.expires(self._latest))) # tick exchange event to handlers await self.processEvent(event) # TODO move out of critical path if self._offline(): # use time of last event self._latest = (event.target.timestamp if hasattr(event, "target") and hasattr(event.target, "timestamp") else self._latest) else: # use now self._latest = datetime.now() # process any secondary events while self._queued_events: event = self._queued_events.popleft() await self.processEvent(event) # process any secondary callback-targeted events (e.g. order fills) # these need to route to a specific callback, # rather than all callbacks while self._queued_targeted_events: strat, event = self._queued_targeted_events.popleft() # send to the generating strategy await self.processEvent(event, strat) # process any periodics await asyncio.gather( *(asyncio.create_task(p.execute(self._latest)) for p in self.manager.periodics() if p.expires(self._latest))) # Before engine shutdown, send an exit event await self.processEvent(Event(type=EventType.EXIT, target=None))
def pushCancel(self, order, accumulate=False, filled_in_txn=0.0): """push order cancellation""" if accumulate: self.accumulate(order, filled_in_txn) self.push(Event(type=EventType.CANCEL, target=order))
def pushChange(self, order, accumulate=False, filled_in_txn=0.0): """push order change""" if accumulate: self.accumulate(order, filled_in_txn) self.push(Event(type=EventType.CHANGE, target=order))
def pushFill(self, order, accumulate=False, filled_in_txn=0.0): """push order fill""" if accumulate: self.accumulate(order, filled_in_txn) self.push(Event(type=EventType.FILL, target=order))
def pushOpen(self, order): """push order open""" self.push(Event(type=EventType.OPEN, target=order))